There are multiple ways of squashing commits but the most recommended and straight forward is to use an interactive rebase, eg

git rebase -i HEAD~4 # interactively rebase from 3 commits ago

This command will bring up your editor with some helpful documentation and list of those commits and messages. The commits are listed in order with the oldest commit at the top and the latest commit at the bottom.

pick f53d15b fixed edge case with IE5
pick 930c0e5 added code coverage
pick fa7c471 fixed lint errors
pick fb57c85 added feature Foo
# Rebase 0a4b808..db5d725 onto 0a4b808
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

To squash the last 4 commits into the most recent, you would change pick to squash for commits 2, 3, and 4.

When the file is saved and the editor is quit, git will automatically squash those 4 commits into one. Your editor
will then pop up again allowing you to change the commit message for the resulting commit(s).

--amend without other options combines the currently staged changes with the last commit and then opens an editor to
update the commit message. If you have staged changes, they will be added.

To update the last message even if there are staged changes (git status reports files under Changes to be committed)
then you can use the -o (or --only) option to indicate you want to amend the last commit but only use the files
that were previously committed.

This will fail if the branch is not merged. To delete the branch regardless:

git branch -D feature

To delete a remote branch feature on remote origin (warning: There is no confirmation!):

git push origin :feature

Note: git-branch documentation lists documentation for the option -r which works on remote tracking branches, not
branches on the remote; git branch -D -r origin/feature will delete the remote tracking branch origin/feature,
not the branch feature on remote origin. Pulling/fetching from the remote again will recreate that tracking branch.

To delete remote tracking branches that no longer exist on the remote:

In git, each commit is tagged with a unique SHA-1 hash. These are critical when resolving differences between sources that
have diverged. This sequence of SHAs is referred to as the “history” and “changing history” is regenerating
those hashes via any number of legitimate means.

A modified history makes it very difficult to resolve differences between two repositories because, from git’s perspective,
the SHAs have changed and commits that should refer to the same changes now look to be two different changes at different times.

Rewriting history has a number of legitimate uses but requires communication proportional to the number of people who
have already based development off the history that is to be changed. It is important that changes are resolved before
history is rewritten and that usually means waiting for all important work to be pushed.

Rewriting local history up to the point of shared changes is rarely a problem. The number of people you need to communicate with
is usually limited to just you.

On shared feature branches, the burden of communication increases to the people who are developing with you on that branch. This
is also usually not a problem if you expect branches to be short lived and highly collaborative (assuming high communication).

On mainline branches that anyone could base off of for any arbitrary work (eg “master” or “develop”),
changing history is very problematic and should only be done in extreme cases where the cost is understood.
Problems occur because you can rarely be sure you’ve communicated with everybody you need to in order to
make sure important changes are pushed before a history modification.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

This is not an error message and is nothing to be worried about. This is simply a notice
that you are not attached to an actual branch. For example, take the following commits on master:

master A-B-C-D-E-F-G

Using the example above where the master branch has commits A-G, if you check out master you will be placed at the ‘tip’
of master, commit G. If you remain at the tip of a branch, you are considered “attached.”

If you check out an arbitrary commit, you are no longer necessarily associated with a branch and are considered “detached.”

This is useful to explore the state of code at a commit, and you can also create and move to a branch from a “detached head” state.

If this state was unintentional, you can get back to where you likely want to be by checking out the branch you expected to be on, eg

HEAD is now at the tip of master. Check out the previous commit by issuing the command git checkout HEAD~1 which references
the commit 1 before HEAD.

$ git checkout HEAD~1
git co HEAD~1
Note: checking out 'HEAD~1'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 183409d... added file.txt

Squashing is the act of turning multiple git commits into fewer. There are many reasons you might want to do this but the most straightforward is to simply keep the commit history cleaner and filled with high value changes.

In a local repository that level of detail may be useful; at some point a regression may have been introduced
while fixing lint errors and you would be able to revert to a previous commit to do some testing.

In the grand scheme of living software, this level of granularity becomes noisier than it is worth. When preparing code
to enter a main line branch it can become more valuable to squash commits into logical chunks so that the history becomes
more valuable as a record of important changes. The following commits could then be squashed into a single commit, eg