Git

From Segfault
Jump to navigation Jump to search

Configuration

Global configuration:[1]

git config --global user.name    "John Doe"
git config --global user.email   john.doe@example.org
git config --global color.pager  false
git config --global pack.threads 0

To send email via git:

git config --global sendemail.smtpserver mail.example.org
git config --global sendemail.smtpserverport 587

Remote repositories

Let's assume we have an existing Github repository:

git clone https://github.com/dummy/foo.git

After committing some changes, let's publish them too:

git remote set-url origin git@github.com:dummy/foo.git
git push

To verify:

$ git config --local -l
[...]
remote.origin.url=git@github.com:dummy/foo.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

Or maybe we don't have a repository yet:

mkdir foobar && cd foobar
echo "Hello, world!" > README.md
git add README.md
git commit -m "bla"

git remote add origin git@github.com:dummy/foobar.git

But before we can push to the repository, we need to create it. After generating a Personal Access Token we can use this to create the repository:

curl -u dummy:${TOKEN} -d '{"name":"foobar"}' https://api.github.com/user/repos

Now the repository should be visible on https://github.com/dummy/foobar and we can push out our local copy:

git push -u origin master

If the generated token has sufficient rights, we can delete the repository again:[2]

curl -u dummy:${TOKEN} -X DELETE https://api.github.com/repos/dummy/foobar

With the now deprecated [3] password authentication API[4], creating the repository would look like this:

curl -u 'dummy:Passw0rd' --header 'x-github-otp: 123456' -d '{"name":"foobar"}' https://api.github.com/user/repos

GH

gh appears to be the more actively maintained command line interface to Github.[5], the other one being hub.

We need a personal access token, configured with appropriate permissions. The token will be stored in ~/.config/gh/hosts.yml:

$ cat ~/.config/gh/hosts.yml 
github.com:
   oauth_token: '386b076a09e5747f6dcd7cffafe40c88d349a020'
   user: dummy
   git_protocol: ssh

Example:

gh repo clone dummy/foobar foobar-git
cd foobar-git

Commit to a new branch:

git checkout -b bugfix-2                                        # Short for: git branch bugfix-2 && git switch bugfix-2
echo "foobar" > README.md 
git add .
git commit -m "another edit"

git push origin -u bugfix-2
gh pr create -t "new PR" -b "This will fix everything"

Merge PR into our master branch:

$ gh pr list
#4  new PR  bugfix-2  about 1 minute ago
$ gh pr merge -d -m 4
✓ Merged pull request #4 (new PR)
✓ Deleted branch bugfix-2 and switched to branch master

Let's try the same for foreign remote repositories, i.e. repositories where we do not have commit access:

gh repo clone someuser/test-repo
cd test-repo
gh repo fork --remote
[edit, commit]

Create a pull request to the original repository:

gh pr create -t "new PR" -b "This will fix everything"

Once the pull request has been opened, we can delete our own fork if we no longer needed it:[6]

$ gh repo delete dummy/test-repo
? Type dummy/test-repo to confirm deletion: dummy/test-repo

Updating an existing PR that we created with gh[7] can be done via plain git. After creating the PR our remotes may look something like this:

$ git remote -v | grep push
fork    https://github.com/my/own-project.git (push)
origin  https://github.com/foobar/some-project.git (push)

The PR exists in foobar/some-project and wants to merge a commit from /my/own-project to foobar/some-project. We will set origin to our own repository:

$ git remote set-url origin https://github.com/my/own-project.git
$ git remote -v | grep push
fork    https://github.com/my/own-project.git (push)
origin  https://github.com/my/own-project.git (push)

Now we can push new commits to our own repository:

$ git push origin branch/fixes 
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 765 bytes | 765.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To https://github.com/my/own-project.git
  691376d3..7b703728  branch/fixes -> branch/fixes

With that, the existing PR will then be updated.

Hub

With hub installed, we can even create pull requests:

git checkout -b feature-2                                        # Short for: git branch feature-2 && git switch feature-2
echo "foobar" >> README.md 
git add .
git commit -m "another edit"

git push origin -u feature-2
hub pull-request

Note: for this to work we need a personal access token, configured with appropriate permissions. The token will be stored in ~/.config/hub:

$ cat ~/.config/hub
github.com:
- user: dummy
  oauth_token: 14bf0d4cb5471417ec56ac099b52d18a421ac2e1
  protocol: https

Merge PR into our master branch:

git checkout master 
hub merge https://github.com/dummy/foobar/pull/1
git push

Let's try the same for foreign remote repositories, i.e. repositories where we do not have commit access:

git checkout -b test
[edit, commit]

Fork into a new repository of ours and push out our changes to that new repo:

hub fork --remote-name origin
git push origin test

Create a pull request to the original repository:

hub pull-request

Once the pull request has been opened, we can delete our own fork if we no longer needed it:[8]

hub delete dummy/forked_repo

Calculate checksum of remote repository

$ git ls-remote origin HEAD
a4c6476651ab11eea605fd09e63acd84bed80284        HEAD
a4c6476651ab11eea605fd09e63acd84bed80284        refs/heads/master
[...]

Calculate checksum of the local repository:

$ git rev-parse HEAD
a4c6476651ab11eea605fd09e63acd84bed80284

Undo a Merge

This is described in detail in the Pro Git book:

git checkout master
git fetch origin                                         # May be needed if there are foreign commits in our tree.
git reset --hard commit-before-the-merge                 # Add origin/master to reset to, well, origin/master

dangling tree

git fsck would complain about a dangling tree:

$ git fsck --full --strict
Checking object directories: 100% (256/256), done.
Checking objects: 100% (400/400), done.
dangling tree 85200871abce3a8aef5136f1221ff0267ecca339
dangling tree 5050f6d17066212c805458709f18e099b921fd59

The tree was working perfectly and there's only one tree in this repo anyway. After git gc only one "dangling tree" was left. Another git prune did the trick, no more dangling trees.

set-url

After cloning a local repository, its origin points to the local resource, instead of the remote resource:

$ git clone foo@alice:/usr/local/src/e2fsprogs-git
$ cd e2fsprogs-git
$ git remote -v show
origin  foo@alice:/usr/local/src/e2fsprogs-git (fetch)
origin  foo@alice:/usr/local/src/e2fsprogs-git (push)

Let's see what the original URL was:

alice$ $ git remote -v
origin  git://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git (fetch)
origin  git://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git (push)

So, let's change our newly created clone to point to the upstream repository:

$ git remote set-url origin git://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git

Rewrite history

Rewriting history[9] can be done via git amend. Changing the last commit is easy:

git commit --amend

Changing a commit that is farther back is tricky. Let's rewrite something within the last 3 commits:

git rebase -i HEAD~3
> pick f396a45 abc
> pick 8b29ab1 xxx
> pick cd27e14 yyy

Note: the order here is different from e.g. "git log" and f396a45 is the oldest commit!

In order to change the oldest commit, replace the "pick" to "edit" next to f396a45 and save-exit the editor. Now we can rewrite this commit with:

git commit --amend

With that, rebase can continue:

git rebase --continue

Altering commit data

Altering various commit entities can be accomplished with git filter-branch[10]. Let's alter the commit date as an example:

$ git log -n 1  | head -3
commit 27dba1e90110fd1e7b2429c8b17021ba537c471b
Author: John Doe <doe@example.org>
Date:   Sun Feb 22 14:51:54 2015 -0800

But the file we just commited was from a while back:

$ ls -lgo file.c
-rwx------ 1 2767 Feb  5 01:14 file.c

So we want to alter the commit history to match that file date:

H=27dba1e90110fd1e7b2429c8b17021ba537c471b
D=2015-02-05T01:14:00                                            # See git-commit(1): DATE FORMATS

Let's get the correct date format too:

D=$(date -r file.c +%Y-%m-%dT%H:%M:%S)

Now we can alter the commit:

$ git filter-branch --env-filter "if [ \$GIT_COMMIT = $H ]; then \
    export GIT_AUTHOR_DATE="$D" export GIT_COMMITTER_DATE="$D"; fi"

The commit date has now been changed (and the commit hash too):

$ git log -n 1  | head -3
commit c510521d1668ff8464e91de96d20fc3b6da70c31
Author: John Doe <doe@example.org>
Date:   Thu Feb 5 01:14:00 2015 -0800

The commiter or author can also be changed[11][12] for a single commit:

H=27dba1e90110fd1e7b2429c8b17021ba537c471b
AN="Me Surname"
AE="me@example.net"
CE="committer@example.net"

git filter-branch --env-filter "if [ \$GIT_COMMIT = $H ]; then \
    export GIT_AUTHOR_NAME="$AN" export GIT_AUTHOR_EMAIL="$AE"; fi"

To rewrite every commit[13] on a branch:

git filter-branch --env-filter "export GIT_AUTHOR_EMAIL=$AE GIT_COMMITTER_EMAIL=$CE" master

git-filter-branch(1) understands GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL and GIT_COMMITTER_DATE

Notes:

Merge commits into one

Multiple commits can be merged[14] like this:

$ git log --pretty=oneline
9ef870fca68ecc80a2f3145ed99ffa3baa5521f1 c
69d2072da98250c5ee9f97359ad582b1e8dceb31 b
5ed10e1c6fc507fcd36955e35d650ee7944700da a

We want to combine commits b & c (the last two commits) into one single commit:

git rebase --interactive HEAD~2
> pick 69d2072 b
> pick 9ef870f c

Change the "pick" to "squash" for the "c" commit, so that it get's combined with its previous commit, b:

> pick 69d2072 b
> squash 9ef870f c

Save & exit from the editor and another editor pops up:

# This is a combination of 2 commits.
# The first commit's message is:
b
# This is the 2nd commit message:
c

Edit as needed and save & quit again. Now the history should look like this:

$ git log --pretty=oneline
98f1c6415f98ff9a12a871bc752b678b41c40e13 b and c
5ed10e1c6fc507fcd36955e35d650ee7944700da a

Convert tarballs into git repository

This can be done[15] as such:

git init
tar -xf ~/foo.tar
git add .
git commit -a -s
rm -rf *

Now the next tarball can be extracted and we continue with "git add" again:

git add .
git commit -a -s
rm -rf *

Continue until all tarballs are integrated into the repository.

Tags

Which tag is a certain commit part of?

git describe --contains c022a0acad534fd5f5d5f17280f6d4d135e74e81
v2.6.36-rc1~300^2~1

So, commit c022a0acad534fd5f5d5f17280f6d4d135e74e81 was introduced in v2.6.36-rc1~300^2~1.

Search

To find the commit that introduced a string[16]:

git log -S foo --source path/to/file                      # Use --all to search the whole repository

Search across all branches:[17]

git rev-list --all | xargs git grep "foo"

Show file at specific revision:[18]

$ git show v2.6.12:Makefile | head -5
VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 12
EXTRAVERSION =
NAME=Woozy Numbat

Diff

Show different versions on the same branch:[19]

$ git diff v2.6.12 v6.2-rc6 Makefile | grep ^.NAME
-NAME=Woozy Numbat
+NAME = Hurr durr I'ma ninja sloth

Git 2.11 introduced the diff.wsErrorHighlight option to show whitespace errors[20] and it works just fine in color terminals, but not on monochrome displays, but we can use cat to help here:

$ git diff --ws-error-highlight=all | cat --show-ends
diff --git a/foo b/foo$
index ce01362..f2aa86d 100644$
--- a/foo$
+++ b/foo$
@@ -1 +1 @@$
-hello$
+hello $

Since Git has such a nice diff interface, and we might not have diffstat installed, we can use it even without a repository:

$ git diff --no-index --stat foo bar
foo => bar | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Abbreviated commits

$ git log --oneline -1 
d8f65e3c46145c342b7f50ec6b672eda02e84092 Fixed that bug.

Abbreviated commits are easier to handle:

$ git config log.abbrevcommit
true

As the man page explains:

> Instead of using the default number of hexadecimal digits (which will vary according to the number of objects
> in the repository with a default of 7) of the abbreviated object name, use <number> digits, or as many digits
> as needed to form a unique object name.

With that we can even find the biggest[21] repositories checked out:

for d in *; do
   printf "${d} "; cd ${d} && git log --oneline -1 | awk '{print $1}'
   cd ..
done | while read -r d c; do
   echo -e "${#c} ${d}"
done | sort -n | tail -5

Repo size without cloning

For Github repos, their API can be used[22]:

$ curl -s https://api.github.com/repos/git/git | awk '/size/ {print $1, $2, "?B"}'
"size": 481892, ?B

...but we don't know the unit size of this value :-\

CVS to Git

While cvs2git may (or may not)[23] be able to do the job, using git-cvsimport was easier to use and actually worked:

$ cat ~/authorconv
foobar=Foo Bar <foobar@example.org>

$ git cvsimport -v -R -d :pserver:cvs@cvs.example.net:/cvs -A ~/authorconv module

This takes quite some time to complete, but in the end:

$ ls -go .git/cvs-*
-rw-r--r--. 1    94 Apr  8 19:55 .git/cvs-authors
-rw-r--r--. 1 68611 Apr  8 20:12 .git/cvs-revisions

$ git log | grep -c ^commit
712

SVN to Git

Migrating a Subversion repository to Git[24] should be quite straighforward.

Generate the list of authers who commited to the SVN repository:

$ cd ~/project-svn
$ svn log --xml | grep \<author\> | perl -pe 's/.*>(.*?)<.*/$1 = /' | sort -u | tee ~/users.txt
alice = 
bob = 

We have to define mappings for these authors to git-svn can use it:

$ cat ~/users.txt
alice = Alice <alice@example.net>
bob = Bob <bob@example.org>

With ~/users.txt in place, we can import the repository:

mkdir ~/project-git && cd ~/project-git
git svn clone svn://code.example.com/svn/ --authors-file=$HOME/users.txt --no-metadata foobar-git

Now the Git repository should be in place:

$ cd ~/foobar-git
$ git log --max-count=1
commit 8b5d45c20bc9cf490e368bc82f058fb4d9739b2c
Author: Alice <alice@example.net>
Date:   Sat Jan 21 19:24:16 2012 +0000

   fixed something

HTTPS checkout

When checking out over HTTPS:

$ git clone https://github.com/ioerror/tlsdate.git
Cloning into 'tlsdate'...
error: Problem with the SSL CA cert (path? access rights?) while accessing https://github.com/ioerror/tlsdate.git/info/refs
fatal: HTTP request failed

We may have to install the CA certificates first:

apt-get install ca-certificates

Track remote branches

In an already cloned repository, do:

git remote add foobar https://github.com/kelseyhightower/nocode

Fetch objects from the new branch, including tags:

git fetch --tags foobar

List all remote branches:

$ git remote 
foobar
origin

Update remote branch, locally:

git remote update foobar

Update all remote branches, locally:

git checkout master
git remote update

Show the remote default branch:

$ git remote show origin | sed -n '/HEAD branch/s/.*: //p'
feature/foobar

Show untracked files

git ls-files . --exclude-standard --others

List ignored files, as per .gitignore:

git ls-files . --ignored --exclude-standard --others

Credential store

To save the login credentials of a password protected repository:

$ git remote -v
origin  https://git.example.net/p/foo.git (fetch)
origin  https://git.example.net/p/foo.git (push)

$ git config credential.helper store

Git will ask one more time for credentials, and then store them on the disk:

$ git pull
Username for 'https://git.example.net': bob
Password for 'https://bob@git.example.net': ***********
Already up to date.

$ git pull
Already up to date.

$ cat ~/.git-credentials 
https://bob:password@git.example.net

To prevent saving credentials to disk, we can store them in memory for some time:

$ rm ~/.git-credentials
$ git config credential.helper 'cache --timeout=36000'

Git will ask one more time for credentials, and then store them in memory for 10 hours:

$ git pull
Username for 'https://git.example.net': bob
Password for 'https://bob@git.example.net': ***********
Already up to date.

$ git pull
Already up to date.

Housekeeping

Remove stale branches[25]:

git fetch --all --prune

Remove local branches no longer present on the remote:

$ ls -1d .git/refs/{heads,remotes}/*{,/*} 2> /dev/null
.git/refs/heads/bugfix
.git/refs/heads/master
.git/refs/remotes/origin

$ git branch -vv | awk '/\[origin.*: gone\]/ {print $1}' | xargs git branch -d

Or, in a more manual fashion:

for x in $(git branch --format='%(refname:short)' | grep -vE 'master|main'); do
   if ! git branch -r | grep -vE 'HEAD|master|main' | grep -q "origin/${x}"; then
        git branch --delete "${x}"
   fi
done

Signing

Signing commits[26] can be done via GPG, S/MIME or SSH, with the latter being the most convenient option these days.[27]

Configure with:

git config --global gpg.format ssh
git config --global user.signingkey ${HOME}/.ssh/foo-key

Enable for all future commits:

git config --global commit.gpgsign true                    # These options are appear to be a misnomer
git config --global tag.gpgsign true

But signatures may still be missing and the following error can be seen:

error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification

We'll have to configure the allowed_signers file:[28]

$ cat ~/.config/git/allowed_signers
user@example.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkt64CuQa3+zoAc94xqnL3N+yebus4upPPwAJJRFHyN snafu

$ git config --global gpg.ssh.allowedSignersFile "${HOME}/.config/git/allowed_signers"

With all that, we sign and then verify commits:

$ git commit -S -m foo

$ git verify-commit $(git log --pretty=%H -1)
Good "git" signature for user@example.net with ED25519 key SHA256:8WVuD4iHEeblvsfetZGlvHqi7yRF918cAXQrhKNSIOA

We could even change all of our existing GPG keys into SSH keys.[29] First, remove all (GPG) signatures:

git filter-branch --commit-filter 'git commit-tree --no-gpg-sign "$@";' -- --all

Maybe git push --force to have the remote side updated as well. And now sign all commits again. Since gpg.format ssh was configured above, this should sign all commits with out SSH key now:

git filter-branch --commit-filter 'git commit-tree --gpg-sign "$@";' -- --all

Note: GPG keys can have an expiration date, and can be revoked too. SSH keys do not have that property. Keep that in mind before changing signatures.

References

  1. Git_Guide: Any other initial setup I need?
  2. REST API v3: Delete a repository
  3. Deprecated APIs and authentication
  4. Working with two-factor authentication
  5. GitHub CLI & hub
  6. Should I keep my GitHub forked repositories around forever?
  7. gh pr create should detect and update already existing pull requests
  8. Should I keep my GitHub forked repositories around forever?
  9. 6.4 Git Tools - Rewriting History
  10. How can one change the timestamp of an old commit in Git?
  11. Changing the committer
  12. Change commit author at one specific commit
  13. Running filter-branch over a range of commits
  14. How can I merge two commits into one?
  15. Is there a way to easily convert a series of tarballs of a source tree into a git repository?
  16. git: finding a commit that introduced a string
  17. Using Git, how could I search for a string across all branches?
  18. How to retrieve a single file from a specific revision in Git?
  19. How do I diff the same file between two different commits on the same branch?
  20. How to show space and tabs with git-diff
  21. AWK sort array of strings by string length
  22. See the size of a github repo before cloning it?
  23. Is there a migration tool from CVS to Git?
  24. Git and Other Systems - Migrating to Git: Subversion
  25. What is the proper way in Git for clean up of stale branches?
  26. Signing commits: You can sign commits locally using GPG, SSH, or S/MIME.
  27. GPG And Me
  28. ssh-keygen: ALLOWED SIGNERS
  29. Is there a way to gpg sign all previous commits?