I hope the title doesn't confuse too much, I'll try to explain the issue I'm having in more detail here:
Suppose you have a repository with a code file (e.g. "main.py") and a setup file (e.g. "setup.env"). Now, the code is universal for everyone working on the repository, but the setup is specific to every contributor. For this example lets say ignoring the setup file and moving the setup in a separate submodule is not an option.
If we now make two branches called "feat-code" and "feat-setup" for the universal and personal changes each, is it possible to have the current HEAD on both branches at once? This would effectively merge both histories, without actually merging and creating a commit.
CodePudding user response:
You can't do that at once, however you can have separate git repository for personal setup. Personally I wouldn't recommend that.
Usually when setup is stored in git repo, it is template, which has to be changed individually. They are added to .gitignore file, so don't worry about changes. If you create new, remember to add it to .gitignore before personalizing it.
CodePudding user response:
No, you can't do that.
HEAD
in Git looks like a branch name, but it isn't: it's a symbol. To some people, this makes more sense when they use Git's one-character synonym @
. The @
means the same thing as HEAD
: it's a symbol that stands for the current branch name.1 So if your current branch is feat-code
, then @
means feat-code
. If your current branch is feat-setup
, then @
means feat-setup
. And HEAD
is just a longer way to write @
.
When you use git switch
(or the older git checkout
in its git switch
mode), you're instructing Git to change the name that @
represents. In the process, you're also telling Git to switch which commit is checked out, except in one special case outlined below. This means remove all the files that come from the current commit; I have picked out the commit I want to use instead, using the branch name I give you on the command line. Take all the files out of that commit.
To make sense of this, you need to know that Git is really all about commits. Most of the things you do—except for the process of making new commits—are ways to say "look at this commit" or "look at that commit" or "extract some commit" or "show me commits starting with some particular commit". So you need to know all about commits:
Each commit is numbered, with a random-looking, big ugly hexadecimal number that is unique to that commit. When you make a new commit, it takes that number, forever; no other commit anywhere can ever use that number again.2
Each commit stores—forever (or as long as the commit itself continues to exist)—two things:
A commit has a snapshot of every file. More precisely, it has a snapshot of all the files Git "knows about" (see below). These files are stored in a compressed, Git-ified, and de-duplicated format. Only Git can read them, and literally nothing—not even Git itself—can overwrite them.
A commit also contains some metadata, such as the name and email address of the person who made it. It has some date-and-time-stamps and some other information. One piece of information is for Git itself: each commit has a list—usually just one element long—of previously existing commits, which Git calls the parent or parents of this commit.
All commits—all Git internal objects, really—are read-only. No part of any commit can ever be changed.
This means you can't actually do anything with a commit. You have to extract one, into a work area. The commit you extract is your current commit, and the working area, where you do your work, now has the files taken out of that commit. Git calls this work area your working tree or work-tree, and the work-tree has an associated current commit and current branch as remembered by the symbol @ or the name
HEAD`.
1There's aso a "detached HEAD" mode, where you're on no branch. Git uses this for git rebase
, and for several other internal modes, and if you want to "go back in time" and look at old commits, you can use this mode. You wouldn't normally do any new work in this mode, though, because new commits you make in this mode are "lost" (in the sense that neither you nor Git can find them) as soon as you pick a branch name again. So working in this mode requires extra care, like Git uses in the middle of an ongoing rebase.
2This is technically impossible, but the huge size of the commit hash ID puts off doomsday. We have to hope that it's put off long enough.
Your working tree and Git's index
When you select a branch to check out, the files from that branch go into Git's index and then into your working tree. So Git now knows about all of those files. They came out of the commit you chose. They're now in Git's index (or staging area) and your working tree. If you create other files in your working tree, Git doesn't know about them, and they won't be in the next commit unless you run git add
on them.
When you run git commit
, Git packages up all the files that are in Git's index (aka the staging area). So if you've modified a working tree file, you need to run git add
on it: this tells Git make the staging area copy match the working tree copy. If you've created an all new file, you must run git add
on it: this again tells Git the same thing. Either way the staging-area (index) copy is now updated to match the working tree copy, and the next commit will have that file.
This means Git's index—or the staging area—acts as your proposed next commit. You see and work on files in your working tree, but you must git add
them to update the commit-proposal.
git worktree add
One very useful trick is that Git allows you to have more than one active working tree at a time. Each working tree has its own HEAD
and index. Git does require that each working tree be on a different branch (or in detached-HEAD mode). The reason for this has to do with how branch names work, which I have not covered yet. Here's a quick summary.
How branch names work
In Git, a branch name simply selects one particular commit. We say that the branch name points to this commit. The name does this by containing the raw hash ID of that one commit.
Remember above that I said that each commit stores the raw hash ID(s) of some previous commit(s), usually just one. This means that commits also point to other commits—backwards, the way Git works:
... <-a123456 <-b789abc <-fedcba9 ...
might be a string of (shortened) hash IDs, with each commit pointing backwards to its parent. If we replace the hash IDs with uppercase letters, we get something more usable by humans:
...--F--G--H <-- somebranch
The name somebranch
contains the hash ID of the latest commit on the branch, which in this case is hash H
. Commit H
contains a snapshot of all files, plus metadata that includes the raw hash ID of earlier commit G
. Commit G
contains a snapshot of files, plus the hash ID of earlier commit F
. This goes on forever, or at least, until we get to the very first commit ever, which—since it can't point backwards to any earlier commit—doesn't. (Its list of previous hash IDs is empty, in other words.)
Now, more than one branch name can select the same commit, like this:
...--G--H <-- main, feature
We need to know which name you're using:
...--G--H <-- main (HEAD), feature
This says you're using commit H
through the name main
: your current branch is main
and its commit is H
. If you run git switch feature
, you get:
...--G--H <-- main, feature (HEAD)
You're still using commit H
, but now you're using it through the name feature
.
So this is the special case where Git doesn't ever have to remove any files: you move from commit H
to, well, commit H
. Obviously they have the same files. So there's nothing to remove-and-replace.
Note that once you make a new commit, the name you're on updates. New commit I
will point back to existing commit H
, but the current branch name now points to new commit I
:
...--G--H <-- main
\
I <-- feature (HEAD)
Make another new commit and you have:
...--G--H <-- main
\
I--J <-- feature (HEAD)
This is the real secret to branches in Git: a name just selects a commit. You can move the names around all you like—they're changeable, and your branch names are yours to do with as you will. The commits are permanent and read-only; they cannot be changed, only found (by names or other commits), or not-found. For instance, if you make J
, but then decide it's terrible, you can eject it from feature
(and Git's index and your working tree) using git reset --hard
, giving:
...--G--H <-- main
\
I <-- feature (HEAD)
\
J [abandoned]
Note that nothing happened to J
: it's still in there. You just can't find it any more.