On Mon, Nov 14, 2022 at 05:57:27PM +0100, Uwe Brauer wrote:

> I am sorry for such an elementary question, but using mainly hg, I found
> the following very confusing.

No, the question is not elementary, and yes, the behavior is confusing.

> I cloned a repository for which 
> 
>  git branch -a 
> 
> Returns
> 
>  git branch -a
> * main
>   remotes/origin/HEAD -> origin/main
>   remotes/origin/feature
>   remotes/origin/main
> 
> This is a github repository where the default branch is now main not master.
> 
> Now I want to commit to the feature branch that has its origin in
> github.
> 
> So 
> 
>  git checkout -b feature 
> is not the right thing since it seems to create a local branch feature.

Yes, it exactly what it did, see below.

> So 
> 
>  git checkout -b   remotes/origin/feature
> 
> Seems to be correct but 

No, it isn't, but bear with me.

>  git branch -a gives me
> 
>   main
> * remotes/origin/feature
>   remotes/origin/HEAD -> origin/main
>   remotes/origin/feature
>   remotes/origin/main
> 
> So it seems the branch is now duplicated? Confusing. Can I
> edit+commit+push without problems? I am afraid to scew up things

No, the branch is not duplicated but it's better to get rid of it anyway,
hold on.

OK, so a couple bits of theory first.

Git has two kinds of branches:

 - "Local" branches is what displayed by unadorned `git branch`.
   They are yours, and in most cases are created by you with may be the only
   obvious exclusion being the `git clone` being run in its default mode,
   which creates a single local branch for you.

 - "Remote" branches are sort of "bookmark" to the state of the branches
   in some named remote repository Git has contacted with - specifically,
   the last time it did so.

The chief difference between these two kinds of branches is that local
branches are for doing development while remote branches are there for
reference purposes only.


Let's now consider naming of the branches.
Git calls all symbolic handles for commits "references". Tags are references,
and so are all kinds of branches. HEAD is also a reference, and there exists a
couple other special references which are like HEAD but which are less
visible, so let's not digress.

All refs do actually have full unambiguous names, but this is generally not
seen - in particular high-level commands such as `git branch` do not expose
this. Still, the thing is, all refs have their "namespaces". In particular,

 - Local branches really have names refs/heads/<branch_name>, so "main"
   is really refs/heads/main.

 - Tags are, perhaps, less surprisingly are refs/tags/<tag_name>.

 - Remote branches are refs/remotes/<remote_name>/<branch_name>.

 - Special refs such as HEAD live at the top level, so HEAD is just HEAD.

Since full names are flexible but are unwieldy for frequent use, Git
implements a simple rule of "resolving" short names designating references
to full names. This is detailed in the "gitrevisions" manual page
(you can run `git help revisions`) - right there in the section "SPECIFYING
REVISIONS". Long story short, when you run `git checkout master`, Git sees
you used "master" in a place where a reference name is expected, sees it
does not look like one of the special refs such as HEAD, and also does not
start with "refs/" and is hence not a full reference name, so it uses its
resolving algorithm and ends up with refs/heads/master, which is then uses
to actually manipulate the repository.

Let's now consider the output of `git branch -a`. The "-a" option told Git
to output all the branches - both local and remote, - wich it did.
While doing so it dropped the special prefixes so that local branches are
listed with their short names and remote branches start with "remotes/".


Let's now move to the

  git checkout -b remotes/origin/feature

command. The behaviour of this command with the "-b" option is documented as
follows:

| git checkout -b|-B <new_branch> [<start point>]
|     Specifying -b causes a new branch to be created as if git-branch(1) were
|     called and then checked out. <...>

Hence basically that command did the following:

  git branch remotes/origin/feature
  git checkout remotes/origin/feature

as you probably know, the `git branch <branch_name>` creates the named branch
pointing to the same commit HEAD points at. So the command has created a
branch with the full name refs/heads/remotes/origin/feature pointing at the
same commit HEAD did at the time that command run.

The name of the branch is perfectly valid albeit confusing: the
prefix-trimming rules of `git branch` makes your new branch and the existing
remote branch look 100% the same (well, if your terminal support colors you
should see the colored output - with the remote branches being rendered in
reddish color by default).

You can verify they are actually different branches by using a low-level
command:

  git for-each-ref --format='%(refname)'

will list all the refs with their no-frills full names.

Note that `git branch -r` shows only remote branches, and `git branch` without
any options shows only local branches.


You can delete this banch using simple

  git branch -d remotes/origin/feature
  

Let's now consider how local and remote branches interact.

Git's model of working with remote repositories is asymmetric: any given
repository can fetch from and push to any other Git repository it is
technically able to communicate with. To highlight the point, nothing prevents
you from fetching everything from, say, github.com/torvalds/linux, into your
local repository containing your weekend toy project web application.
>From this follows, that the fact some branch is named "master" in a given
repository means nothing for a branch named "master" in your local repository;
continuing my example, the branch "master" in your weekend project repo has
nothing in common with the same-named branch you have fetched from the
repository tracking the development of Linux. This is where the asymmetry is
rooted.
Still, it's convenient to have certain local branches "linked" to certain
branches in other Git repositories. For instance, in now-popular workflows
involving a single centralized rendez-vouz repository everyone fetches from
and pushes to, its sensible to have, say, your local branch "master" be
somehow tied to the branch "master" in that remote repository.
Enter branch tracking ;-)
Any local branch in a Git repository can be "set to track" a single remote
branch fetched from a named remote repository. This is precisely what happens
when you `git clone` without any special options: Git picks the branch
designated as "main" (this is not a name but a concept), creates a local
branch with the same name that main branch has and sets it to track that
remote branch.
Tracking is a purely configuration thing.

Tracking a remote branch is useful because it trivializes questions like "what
commits my branch has compared to the recorded state of this branch in the
remote repository?", and allows to easily synchronize your local state with
the updated remote state.

Unfortunately, Git has confusing terminology here: when you make a local
branch track a remote branch, the former is named "remote-tracking branch"
because it tracks a remote branch.


Let's now add the final piece to this picture.

You can read in any book or guide that you never "work on" remote branches,
but only on local branches. This is not the full truth but it's a good sane
default mindset to maintain.
When you check out a local branch using `git checkout <branch_name>`, Git
makes the HEAD ref point at the (full) name of the checked out branch, and 
because of this recording a new commit makes that named branch be promoted to
point to the new commit.
When you check a remote branch, Git makes the HEAD ref point directly at the
commit which that branch points at - a situation known as "detached HEAD".
Recording new commits will update just the HEAD, and not any branch.
This is a pretty normal situation for seasoned Git uses (I, for one, mostly
work in this state) but it's not recommended for inexperienced users: I have
described what happens to have you prepared for the case which would occur
if you were to run

  git checkout origin/feature

as you would end in that detached HEAD state.

OK, so what you do if you want to do work based on a non-default branch in a
remote repository?
If we return to your example with the branch named "feature", the simplest
approach would be to just run

  git checkout feature

Git would notice there is no local branch named "feature" but there exist a
remote branch named "feature", and would create a local branch "feature"
pointing at the same commit the remote one points at, and tracking that remote
branch.

Alternatively, you could do that explicitly without involving any magic
of the short-circuit command invocation:

  git branch feature origin/feature # create local branch off a remote one
  git checkout feature              # check local branch out
  git branch -u origin/feature      # make current branch track origin/feature


After recording a couple of commits, you run

  git push origin feature

With default configuration settings, it will try to update the branch named
"feature" in the remote repository known locally as "origin" with the commits
from your local branch "feature".

-- 
You received this message because you are subscribed to the Google Groups "Git 
for human beings" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to git-users+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/git-users/20221115153626.zbaxz7ysd2fao6ty%40carbon.

Reply via email to