git: add FAQ

Add in the FAQ from the github docs I did for the transition.

Submissions from: rpokala@, jrm@, Marc Branchaud and "mirror176".
This commit is contained in:
Warner Losh 2021-03-18 15:55:16 -06:00
parent 28d4e1377a
commit 3bbe81a675

View file

@ -1624,6 +1624,416 @@ author recommends that your upstream github repo remain the default
push location so that you only push things into FreeBSD you intend to
by making it explicit.
[[git-faq]]
=== Git FAQ
This section provides a number of targeted answers to questions that are likely to come up often for users, developer and integrators.
Note, we use the common convention of having the origin for the FreeBSD repo being 'freebsd' rather than the default 'origin' to allow
people to use that for their own development and to minimize "whoopse" pushes to source of truth.
==== Users
===== How do I track -current and -stable with only one copy of the repo?
**Q:** Although disk space is not a huge issue, it's more efficient to use
only one copy of the repository. With SVN mirroring, I could checkout
multiple trees from the same repo. How do I do this with Git?
**A:** You can use Git worktrees. There's a number of ways to do this,
but the simplest way is to use a clone to track -current, and a
worktree to track stable releases. While using a 'bare repository'
has been put forward as a way to cope, it's more complicated and will not
be documented here.
First, you need to clone the FreeBSD repository, shown here cloning into
`freebsd-current` to reduce confusion. $URL is whatever mirror works
best for you:
[source,shell]
....
% git clone -o freebsd --config remote.freebsd.fetch='+refs/notes/*:refs/notes/*' $URL freebsd-current
....
then once that's cloned, you can simply create a worktree from it:
[source,shell]
....
% cd freebsd-current
% git worktree add ../freebsd-stable-12 stable/12
....
this will checkout `stable/12` into a directory named `freebsd-stable-12`
that's a peer to the `freebsd-current` directory. Once created, it's updated
very similarly to how you might expect:
[source,shell]
....
% cd freebsd-current
% git checkout main
% git pull --ff-only
# changes from upstream now local and current tree updated
% cd ../freebsd-stable-12
% git merge --ff-only freebsd/stable/12
# now your stable/12 is up to date too
....
I recommend using `--ff-only` because it's safer and you avoid
accidentally getting into a 'merge nightmare' where you have an extra
change in your tree, forcing a complicated merge rather than a simple
one.
Here's https://adventurist.me/posts/00296 a good writeup that goes into more detail.
==== Developers
===== Ooops! I committed to `main` instead of a branch.
**Q:** From time to time, I goof up and commit to main instead of to a
branch. What do I do?
**A:** First, don't panic.
Second, don't push. In fact, you can fix almost anything if you
haven't pushed. All the answers in this section assume no push
has happened.
The following answer assumes you committed to `main` and want to
create a branch called `issue`:
[source,shell]
....
% git branch issue # Create the 'issue' branch
% git reset --hard freebsd/main # Reset 'main' back to the official tip
% git checkout issue # Back to where you were
....
===== Ooops! I committed something to the wrong branch!
**Q:** I was working on feature on the `wilma` branch, but
accidentally committed a change relevant to the `fred` branch
in 'wilma'. What do I do?
**A:** The answer is similar to the previous one, but with
cherry picking. This assumes there's only one commit on wilma,
but will generalize to more complicated situations. It also
assumes that it's the last commit on wilma (hence using wilma
in the `git cherry-pick` command), but that too can be generalized.
[source,shell]
....
# We're on branch wilma
% git checkout fred # move to fred branch
% git cherry-pick wilma # copy the misplaced commit
% git checkout wilma # go back to wilma branch
% git reset --hard HEAD^ # move what wilma refers to back 1 commit
....
Git experts would first rewind the wilma branch by 1 commit, switch over to
fred and then use `git reflog` to see what that 1 deleted commit was and
cherry-pick it over.
**Q:** But what if I want to commit a few changes to `main`, but
keep the rest in `wilma` for some reason?
**A:** The same technique above also works if you are wanting to
'land' parts of the branch you are working on into `main` before the
rest of the branch is ready (say you noticed an unrelated typo, or
fixed an incidental bug). You can cherry pick those changes into main,
then push to the parent repo. Once you've done that, cleanup couldn't
be simpler: just `git rebase -i`. Git will notice you've done
this and skip the common changes automatically (even if you had to
change the commit message or tweak the commit slightly). There's no
need to switch back to wilma to adjust it: just rebase!
**Q:** I want to split off some changes from branch `wilma` into branch `fred`
**A:** The more general answer would be the same as the
previous. You'd checkout/create the `fred` branch, cherry pick the
changes you want from `wilma` one at a time, then rebase `wilma` to
remove those changes you cherry picked. `git rebase -i main wilma`
will toss you into an editor, and remove the `pick` lines that
correspond to the commits you copied to `fred`. If all goes well,
and there are no conflicts, you're done. If not, you'll need to
resolve the conflicts as you go.
The other way to do this would be to checkout `wilma` and then create
the branch `fred` to point to the same point in the tree. You can then
`git rebase -i` both these branches, selecting the changes you want in
`fred` or `wilma` by retaining the pick likes, and deleting the rest
from the editor. Some people would create a tag/branch called
`pre-split` before starting in case something goes wrong in the split,
you can undo it with the following sequence:
[source,shell]
....
% git checkout pre-split # Go back
% git branch -D fred # delete the fred branch
% git checkout -B wilma # reset the wilma branch
% git branch -d pre-split # Pretend it didn't happen
....
the last step is optional. If you are going to try again to
split, you'd omit it.
**Q:** But I did things as I read along and didn't see your advice at
the end to create a branch, and now `fred` and `wilma` are all
screwed up. How do I find what `wilma` was before I started. I don't
know how many times I moved things around.
**A:** All is not lost. You can figure out it, so long as it hasn't
been too long, or too many commits (hundreds).
So I created a wilma branch and committed a couple of things to it, then
decided I wanted to split it into fred and wilma. Nothing weird
happened when I did that, but let's say it did. The way to look at
what you've done is with the `git reflog`:
[source,shell]
....
% git reflog
6ff9c25 (HEAD -> wilma) HEAD@{0}: rebase -i (finish): returning to refs/heads/wilma
6ff9c25 (HEAD -> wilma) HEAD@{1}: rebase -i (start): checkout main
869cbd3 HEAD@{2}: rebase -i (start): checkout wilma
a6a5094 (fred) HEAD@{3}: rebase -i (finish): returning to refs/heads/fred
a6a5094 (fred) HEAD@{4}: rebase -i (pick): Encourage contributions
1ccd109 (freebsd/main, main) HEAD@{5}: rebase -i (start): checkout main
869cbd3 HEAD@{6}: rebase -i (start): checkout fred
869cbd3 HEAD@{7}: checkout: moving from wilma to fred
869cbd3 HEAD@{8}: commit: Encourage contributions
...
%
....
Here we see the changes I've made. You can use it to figure out where
things when wrong. I'll just point out a few things here. The first
one is that HEAD@{X} is a 'commitish' thing, so you can use that as an
argument to a command. Though if that command commits anything to the
repo, the X numbers change. You can also use the hash (first column)
as well.
Next 'Encourage contributions' was the last commit I did to `wilma`
before I decided to split things up. You can also see the same hash is
there when I created the `fred` branch to do that. I started by
rebasing `fred` and you see the 'start', each step, and the 'finish'
for that process. While we don't need it here, you can figure out
exactly what happened. Fortunately, to fix this, you can follow the
prior answer's steps, but with the hash `869cbd3` instead of
`pre-split`. While that set of a bit verbose, it's easy to remember
since you're doing one thing at a time. You can also stack:
[source,shell]
....
% git checkout -B wilma 869cbd3
% git branch -D fred
....
and you are ready to try again. The 'checkout -B' with the hash combines
checking out and creating a branch for it. The -B instead of -b forces
the movement of a pre-existing branch. Either way works, which is what's
great (and awful) about Git. One reason I tend to use `git checkout -B xxxx hash`
instead of checking out the hash, and then creating / moving the branch
is purely to avoid the slightly distressing message about detached heads:
[source,shell]
....
% git checkout 869cbd3
M faq.md
Note: checking out '869cbd3'.
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 869cbd3 Encourage contributions
% git checkout -B wilma
....
this produces the same effect, but I have to read a lot more and severed heads
aren't an image I like to contemplate.
===== Ooops! I did a 'git pull' and it created a merge commit, what do I do?
**Q:** I was on autopilot and did a 'git pull' for my development tree and
that created a merge commit on the mainline. How do I recover?
**A:** This can happen when you invoke the pull with your development branch
checked out.
Right after the pull, you will have the new merge commit checked out. Git
supports a `HEAD^#` syntax to examine the parents of a merge commit:
[source,shell]
....
git log --oneline HEAD^1 # Look at the first parent's commits
git log --oneline HEAD^2 # Look at the second parent's commits
....
From those logs, you can easily identify which commit is your development
work. Then you simply reset your branch to the corresponding `HEAD^#`:
[source,shell]
....
git reset --hard HEAD^2
....
**Q:** But I also need to fix my 'main' branch. How do I do that?
**A:** Git keeps track of the remote repository branches in a `freebsd/`
namespace. To fix your 'main' branch, just make it point to the remote's
'main':
[source,shell]
....
git branch -f main freebsd/main
....
There's nothing magical about branches in git: they are just labels on a DAG
that are automatically moved forward by making commits. So the above works
because you're just moving a label. There's no metadata about the branch
that needs to be preserved due to this.
===== Mixing and matching branches
**Q:** So I have two branches `worker` and `async` that I'd like to combine into one branch called `feature`
while maintaining the commits in both.
**A:** This is a job for cherry pick.
[source,shell]
....
% git checkout worker
% git checkout -b feature # create a new branch
% git cherry-pick main..async # bring in the changes
....
You now have one branch called `feature`. This branch combines commits
from both branches. You can further curate it with `git rebase`.
**Q:** OK Wise Guy. That was too easy. I have a branch called `driver` and I'd like
to break it up into `kernel` and `userland` so I can evolve them separately and commit
each branch as it becomes ready.
**A:** This takes a little bit of prep work, but `git rebase` will do the heavy
lifting here.
[source,shell]
....
% git checkout driver # Checkout the driver
% git checkout -b kernel # Create kernel branch
% git checkout -b userland # Create userland branch
....
Now you have two identical branches. So, it's time to separate out the commits.
We'll assume first that all the commits in `driver` go into either the `kernel`
or the `userland` branch, but not both.
[source,shell]
....
% git rebase -i main kernel
....
and just include the changes you want (with a 'p' or 'pick' line) and
just delete the commits you don't (this sounds scary, but if worse
comes to worse, you can throw this all away and start over with the
`driver` branch since you've not yet moved it).
[source,shell]
....
% git rebase -i main userland
....
and do the same thing you did with the `kernel` branch.
**Q:** Oh great! I followed the above and forgot a commit in the `kernel` branch.
How do I recover?
**A:** You can use the `driver` branch to find the hash of the commit is missing and
cherry pick it.
[source,shell]
....
% git checkout kernel
% git log driver
% git cherry-pick $HASH
....
**Q:** OK. I have the same situation as the above, but my commits are all mixed up. I need
parts of one commit to go to one branch and the rest to go to the other. In fact, I have
several. Your rebase method to select sounds tricky.
**A:** In this situation, you'd be better off to curate the original branch to separate
out the commits, and then use the above method to split the branch.
So let's assume that there's just one commit with a clean tree. You
can either use `git rebase` with an `edit` line, or you can use this
with the commit on the tip. The steps are the same either way. The
first thing we need to do is to back up one commit while leaving the
changes uncommitted in the tree:
[source,shell]
....
% git reset HEAD^
....
Note: Do not, repeat do not, add `--hard` here since that also removes the changes from your tree.
Now, if you are lucky, the change needing to be split up falls entirely along file lines. In that
case you can just do the usual `git add` for the files in each group than do a `git commit`. Note:
when you do this, you'll lose the commit message when you do the reset, so if you need it for
some reason, you should save a copy (though `git log $HASH` can recover it).
If you are not lucky, you'll need to split apart files. There's another tool to do that which you
can apply one file at a time.
[source,shell]
....
git add -i foo/bar.c
....
will step through the diffs, prompting you, one at time, whether to include or exclude the hunk.
Once you're done, `git commit` and you'll have the remainder in your tree. You can run it
multiple times as well, and even over multiple files (though I find it easier to do one file at a time
and use the `git rebase -i` to fold the related commits together).
==== Cloning and Mirroring
**Q:** I'd like to mirror the entire git repo, how do I do that?
**A:** If all you want to do is mirror, then
[source,shell]
....
% git clone --mirror $URL
....
will do the trick. However, there are two disadvantages to this if you
want to use it for anything other than a mirror you'll reclone.
First, this is a 'bare repo' which has the repository database, but no
checked out worktree. This is great for mirroring, but terrible for day to
day work. There's a number of ways around this with 'git worktree':
[source,shell]
....
% git clone --mirror https://cgit-beta.freebsd.org/ports.git ports.git
% cd ports.git
% git worktree add ../ports main
% git worktree add ../quarterly branches/2020Q4
% cd ../ports
....
But if you aren't using your mirror for further local clones, then it's a poor match.
The second disadvantage is that Git normally rewrites the refs (branch name, tags, etc)
from upstream so that your local refs can evolve independently of
upstream. This means that you'll lose changes if you are committing to
this repo on anything other than private project branches.
**Q:** So what can I do instead?
**A:** Well, you can stuff all of the upstream repo's refs into a private
namespace in your local repo. Git clones everything via a 'refspec' and
the default refspec is:
[source,shell]
....
fetch = +refs/heads/*:refs/remotes/freebsd/*
....
which says just fetch the branch refs.
However, the FreeBSD repo has a number of other things in it. To see
those, you can add explicit refspecs for each ref namespace, or you
can fetch everything. To setup your repo to do that:
[source,shell]
....
git config --add remote.freebsd.fetch '+refs/*:refs/freebsd/*'
....
which will put everything in the upstream repo into your local repo's
'res/freebsd/' namespace. Please note, that this
also grabs all the unconverted vendor branches and the number of refs
associated with them is quite large.
You'll need to refer to these 'refs' with their full name because they
aren't in and of Git's regular namespaces.
[source,shell]
....
git log refs/freebsd/vendor/zlib/1.2.10
....
would look at the log for the vendor branch for zlib starting at 1.2.10.
[[subversion-primer]]
== Subversion Primer