Basic Usage of git and gh with GitHub

On macOS, I mostly use GitHub Desktop instead of git on the command line. Both my GitHub OAuth Token and Credentials are stored in the macOS KeyChain. This setup was configured a long time ago. I don’t even remember if I used git-credential-osxkeychain or not.

I always wanted to learn git. For this post, I’ll create a new GitHub account, and set it up with SSH on an Alpine Linux VM.

Setup GitHub CLI (optional)

Download the binary for your OS here. Run gh auth login to start the authentication process. Select HTTPS as the protocol, and follow the rest of the instructions:

alpine:~# gh auth login
? What account do you want to log into? GitHub.com
? What is your preferred protocol for Git operations? HTTPS
? How would you like to authenticate GitHub CLI? Paste an authentication token
Tip: you can generate a Personal Access Token here https://github.com/settings/tokens
The minimum required scopes are 'repo', 'read:org', 'workflow'.
? Paste your authentication token: ****************************************
- gh config set -h github.com git_protocol https
✓ Configured git protocol
✓ Logged in as kgrtest

The oauth token will be stored in $HOME/.config/gh/hosts.yml. Keep it safe.

Setup SSH

Generating the keys:

ssh-keygen -C "kavishhunter01@gmail.com"

Browse to your github account and add the public key. Or use gh:

alpine:~/test_github_sshkeys# gh ssh-key add testkey.pub -t "Test Key"
✓ Public key added to your account

Test the connection:

alpine:~/test_github_sshkeys# eval "$(ssh-agent -s)"
Agent pid 4885
alpine:~/test_github_sshkeys# ssh-add testkey
Enter passphrase for testkey:
Identity added: testkey (kavishhunter01@gmail.com)
alpine:~/test_github_sshkeys#
alpine:~/test_github_sshkeys# ssh -T git@github.com
Hi kgrtest! You've successfully authenticated, but GitHub does not provide shell access.

Note: SSH Keys that were added with gh, will be removed when the token specified during gh auth login is deleted or if you re-ran the authentication process.

On macOS

The ssh-add utility that comes with macOS has two special options: -AK to store your SSH Keys passphrase in the macOS KeyChain. You’ll also need to modify ~/.ssh/config. You can read more about it here.

Fork, Branching, and Pull Requests

To be able to send a pull request, you have to fork the repo you’ll be working on, clone it locally, switch to a new branch, commit your new changes, and push the new branch to your origin. Then you log in to your github account, browse to your fork, and open the new pull request for the upstream repo.

Origin: Is your fork that you have cloned locally. Upstream: Is the repo that you have forked from, hence the source of truth.

Most of your work will take place on origin. The origin is the only repo that you have permission to modify. Communication with upstream is to only fetch/pull new changes or to open a pull request.

Fork and Clone

The upstream is kavishgr/Test-Repo and the origin is kgrtest/Test-Repo.

When you fork a repo on github, and clone it with git, the origin shortcut is added automatically:

alpine:~# git clone git@github.com:kgrtest/Test-Repo.git
Cloning into 'Test-Repo'...
...
...
...
...
alpine:~#
alpine:~# cd Test-Repo/
alpine:~/Test-Repo#
alpine:~/Test-Repo# git remote -v
origin	git@github.com:kgrtest/Test-Repo.git (fetch)
origin	git@github.com:kgrtest/Test-Repo.git (push)
alpine:~/Test-Repo#

Origin is pointing to the forked repo: kgrtest/Test-Repo.git

With gh, upstream is configured by default:

alpine:~/test# gh repo clone kgrtest/Test-Repo
Cloning into 'Test-Repo'...
...
...
...
Updating upstream
From https://github.com/kavishgr/Test-Repo
 * [new branch]      main       -> upstream/main
alpine:~/test#
alpine:~/test# cd Test-Repo/ ; git remote -v
origin	https://github.com/kgrtest/Test-Repo.git (fetch)
origin	https://github.com/kgrtest/Test-Repo.git (push)
upstream	https://github.com/kavishgr/Test-Repo.git (fetch)
upstream	https://github.com/kavishgr/Test-Repo.git (push)
alpine:~/test/Test-Repo#

To set the upstream, run git remote add upstream [URL]:

alpine:~/Test-Repo# git remote add upstream git@github.com:kavishgr/Test-Repo.git
alpine:~/Test-Repo# git remote -v
origin	git@github.com:kgrtest/Test-Repo.git (fetch)
origin	git@github.com:kgrtest/Test-Repo.git (push)
upstream	git@github.com:kavishgr/Test-Repo.git (fetch)
upstream	git@github.com:kavishgr/Test-Repo.git (push)
alpine:~/Test-Repo#

Set username and email:

alpine:~/Test-Repo# git config user.name "Test user"
alpine:~/Test-Repo# git config user.email "test@email.com"

Branches

By convention, you should switch to a new branch to add new changes, and then push the new branch. If you’re working on a solo project you can just push your code to origin.

Create a new branch with git checkout -b:

alpine:~/Test-Repo# git checkout -b quick-test
Switched to a new branch 'quick-test'

The new branch quick-test is created.

Push new branch to create a pull request

Add new changes and commit:

alpine:~/Test-Repo# echo "A new line" > dummyfile.txt
alpine:~/Test-Repo#
alpine:~/Test-Repo# git add .
alpine:~/Test-Repo# git commit -m "added a dummy file"
[quick-test 96eb77b] added a dummy file
 1 file changed, 1 insertion(+)
 create mode 100644 dummyfile.txt

If you don’t commit you new changes inside the new branch, the changes will reflect in all branches that are currently available.

Push quick-test to your origin:

alpine:~/Test-Repo# git push origin quick-test
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 289 bytes | 48.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'quick-test' on GitHub by visiting:
remote:      https://github.com/kgrtest/Test-Repo/pull/new/quick-test
remote:
To github.com:kgrtest/Test-Repo.git
 * [new branch]      quick-test -> quick-test

A pull request is automatically created for you. Go to your github account, and create the pull request. Once it’s merged, you need to keep your origin and local repo in sync with upstream. The commit that took place in quick-test, only happend in that particular branch. The main or master branch is not aware of that:

### quick-test
alpine:~/Test-Repo# git log

commit 7212a7dcbc678b87ca9fc68654ef2c4ae1120824 (HEAD -> quick-test, origin/quick-test)
Author: Test user <test@email.com>
Date:   Wed Oct 6 11:22:03 2021 +0400

    added a dummy file

commit 8213af3f355712bdbdb98d0bc85e733cb2b380c8 (origin/main, origin/HEAD, main)
Author: Kavish Gour <kavishgr@protonmail.com>
Date:   Wed Oct 6 11:15:26 2021 +0400

    Initial commit


### main
alpine:~/Test-Repo# git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
alpine:~/Test-Repo#
alpine:~/Test-Repo# git log

commit 8213af3f355712bdbdb98d0bc85e733cb2b380c8 (HEAD -> main, origin/main, origin/HEAD)
Author: Kavish Gour <kavishgr@protonmail.com>
Date:   Wed Oct 6 11:15:26 2021 +0400

    Initial commit

You need to fetch the changes on upstream to your local repo(clone) and push it to your origin(fork). On GitHub you can click fetch upstream. I’m gonna do it manually. Switch to your main/master branch, and fetch all the commits from both origin and upstream(including deleted ones):

alpine:~/Test-Repo# git fetch --all --prune
Fetching origin
Fetching upstream
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), 625 bytes | 625.00 KiB/s, done.
From github.com:kavishgr/Test-Repo
 * [new branch]      main       -> upstream/main

Now, reset the main branch on origin to the main branch on upstream:

alpine:~/Test-Repo# git reset --hard upstream/main
HEAD is now at 24fda2a Merge pull request #1 from kgrtest/quick-test
alpine:~/Test-Repo#

The logs should now contains all the commits:

alpine:~/Test-Repo# git log

commit 24fda2a0864625da8e09266e40283754ab448be7 (HEAD -> main, upstream/main)
Merge: 8213af3 7212a7d
Author: Kavish Gour <kavishgr@protonmail.com>
Date:   Wed Oct 6 11:22:54 2021 +0400

    Merge pull request #1 from kgrtest/quick-test

    added a dummy file

commit 7212a7dcbc678b87ca9fc68654ef2c4ae1120824 (origin/quick-test, quick-test)
Author: Test user <test@email.com>
Date:   Wed Oct 6 11:22:03 2021 +0400

    added a dummy file

commit 8213af3f355712bdbdb98d0bc85e733cb2b380c8 (origin/main, origin/HEAD)
Author: Kavish Gour <kavishgr@protonmail.com>
Date:   Wed Oct 6 11:15:26 2021 +0400

    Initial commit

Push the changes to your origin(fork):

alpine:~/Test-Repo# git push origin main
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:kgrtest/Test-Repo.git
   8213af3..24fda2a  main -> main
alpine:~/Test-Repo#

Now, origin and upstream are the same. This is how it’s done manually. Another command called git pull does the same thing(fetch and reset). Let’s say there’re new changes on the upstream. You can just pull the changes locally:

alpine:~/Test-Repo# git pull upstream main
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 688 bytes | 137.00 KiB/s, done.
From github.com:kavishgr/Test-Repo
 * branch            main       -> FETCH_HEAD
   24fda2a..8c3e6f1  main       -> upstream/main
Updating 24fda2a..8c3e6f1
Fast-forward
 dummyfile-2.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 dummyfile-2.txt

And push it to your origin:

alpine:~/Test-Repo# git push origin main
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:kgrtest/Test-Repo.git
   24fda2a..8c3e6f1  main -> main

My gh aliases

You can do a lot of stuff with gh just as you would on GitHub. My list of aliases:

MacBook-Pro:~ kavish$ gh alias list
co:                 pr checkout
del-ssh-key-by-id:  api -X DELETE "user/keys/$1"
get-ssh-key-id:     api -X GET "user/keys"
repo-delete:        api -X DELETE "repos/$1"

I work a lot with ssh keys. This just make things a lot more easier. Run the following to set them:

$ gh alias set repo-delete 'api -X DELETE "repos/$1"'
$ gh alias set get-ssh-key-id 'api -X GET "user/keys"'
$ gh alias set del-ssh-key-by-id 'api -X DELETE "user/keys/$1"'

Give your oauth token the necessary permissions:

$ gh auth refresh -s read:public_key
$ gh auth refresh -s delete_repo

I found the repo-delete alias here. You can setup your own aliases by using the GitHub REST api as a reference.

Delete ssh keys with gh

To delete an ssh key, you’ll need its id. You can pipe gh get-ssh-key-id to gron to get both the id and title with sed:

$ echo ; gh get-ssh-key-id | gron | egrep "id|title" | gsed '/title/a -----' | cut -d "." -f 2 | tr -d ';'

Note: I’m using gsed here. On Linux just sed. On macOS, you can download gnu-sed with homebrew.

The output will look like this:

id = 57542413
title = "macOS test key"
-----
id = 57578083
title = "Dummy key"
-----

It’s not that elegant, but it gets the job done. Now you can delete an ssh key like so:

$ gh del-ssh-key-by-id 57578083

“If you need to invoke your academic pedigree or job title for people to believe what you say, then you need a better argument.”Neil deGrasse Tyson