92 comments

[ 0.20 ms ] story [ 172 ms ] thread
I wonder if this might be an educational issue? Maybe people are learning "convenient lies" about how Git works so they can get started, and don't move forward to learning the real truth of the matter?

Just speaking from the perspective of someone that's used git since nearly the very beginning (and CVS and Subversion before that), Git's strong adherence to the Unix philosophy ("give enough rope to hang yourself") was definitely a breath of fresh air compared to the restrictive systems.

What's the convenient lie taught about branches?

The standard concept that I remember is 'a branch is a pointer to a commit', which still feels accurate

Given TFA (a comic) has "you might expect git to enforce some rules about branches", I figured there might be an education problem going on. I didn't expect such things, but being there from the earliest days might be why. I don't even think reset and rebase are "weird stuff", for that matter. I use them multiple times a week.
I’m not GP, but the main one I see if the mental model that a commit is a full representation of the contents of the repo at the time that commit was created (rather than being a change set or deltas between commits).
Right in this subthread people have said that. I now have no idea what is right. Which seems to only support the comics assertion, which was only "you might think x but you'd be wrong"
originally every commit was a full snapshot of the current state of the repository. the repository is now compressed after the fact using deltas but like another person said but it’s really just an implementation detail.
Um, I'm pretty sure that one's not a lie.

Yes, git currently stores deltas between commits in its pack files, but that's an implementation detail to save space. Importantly, it's not how git was originally written, but was added at a later time (although, still a long time ago now).

Edit: https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3...

The way I'm reading the comic, it'd be something like "main/master is special", and "always merge in changes". Things that make people assume that forward history is somehow enforced by the software.

But yeah fixing up work and cleaning up a branch prior to merging is a feature not a bug. Git got it right.

Other people did not get the education you got. Thus, this being a problem with education.

People say things like "a branch does this" when they mean "We have rules on the git server that enforce this", or when they mean "We have a convention to do this, you have to keep track of it". One word to describe this is synecdoche - a part represents the whole or vice versa. Some part of the system does something, or some part of the PROCESS does something, and so people say "git does this thing".

Imprecise speech is shorter, but it can waste a great deal of time in the long run.

It's accurate in most common workflows, but definitely a convenient lie. Consider the case where I create a new branch B from A then remove a commit. The branch is now more like a "a pointer to a commit on a DAG" where branch B points to a commit on a different DAG than branch A.
By "remove a commit" do you mean rebase A to point at its previous commit, HEAD~1?

A commit SHA represents the entire DAG up until that point, not a given changeset. So in your example, branch B is still a pointer to a commit, and branch A is a pointer to a different commit. Where is the convenient lie?

That is one way to “remove a commit” and I agree with your analysis for that case.

The method I had in mind is an interactive rebase. Assume there are n > 5 commits on my branch, choosing 5 arbitrarily. If I run “git rebase -i HEAD~5” then use the “drop” command on the third-most-recent commit, I do end up with a different DAG. The change is subtractive and not just the removal of the current HEAD.

This kind of history change is definitely a bit less common but not obscure by any means. I’ve rebased to drop commits on my own feature branches many times.

Ah ok, understood.

I would argue that is still not a convenient lie of "a branch is a pointer to a commit". Rather, the rebase command is changing what the branch is pointing to, and git tries to make that clear.

After that rebase command, there is a message like `Successfully rebased and updated refs/heads/yourbranch`, showing that the branch named yourbranch is now pointing at something new.

Your original comment:

>Consider the case where I create a new branch B from A then remove a commit. The branch is now more like a "a pointer to a commit on a DAG" where branch B points to a commit on a different DAG than branch A.

My counterpoint taking this to the extreme:

>Consider the case where I create a new branch B from A then run git reset --hard SOMESHA1

In my example, again the new branch is pointing at a completely different commit--because I am repointing the branch.

>I wonder if this might be an educational issue?

It certainly is. There are two things that a team needs to do to use git: 1) Team members all learn git 2) A few conventions are agreed upon to facilitate collaboration

People use #2 to replace #1. And that's why we've had to sit through a neverending flow of blog posts obsessing over git workflows.

Git is flexible to whatever your business workflow is, play around with it a bit, choose something, then make changes when it's not working.

The topic fundamentally does not need this many explainers. But if you're supplanting understanding the tool, it does.

It's best to think of git as a suite of primitives for building a VCS, not a VCS itself. For larger teams, you should pretty much expect to have a dedicated integrator (or integration team) with git scripts for creating branches, PRs, etc.
I suspect it's caused by Git being such a leaky abstraction.

No matter what convenient lie you use in learning it will trip up someone, sooner or later.

> Unix philosophy ("give enough rope to hang yourself") was

That and RTFM are probably reasons why *Nix will always be a bit player when it comes to anything that has contact with people.

And, yes, Android runs Linux, so it doesn't count as an OS. I wouldn't call JVM or Spider Monkey my OS either.

I missed the part on why should git branches have rules in the first place?
Usually software should make it hard to do the wrong thing and make it easy to do the right thing. This is git we are talking about so it is fair to expect it to have a poor UX.
> wrong thing

Rebase/reset are major and first class features of Git. It's only wrong if you work with a team that expect the centralized server to be consistent.

You are assuming git's design is good. Being a first class feature does not mean much if the feature doesn't work towards the user's goals.
For me this is a feature, and so it should be for you as well. Freedom, responsibility..
One interesting, slightly related thing about git branching I run into every once in a while:

   $ git checkout -b test
   Switched to a new branch 'test'
   $ git checkout -b test/1
   fatal: cannot lock ref 'refs/heads/test/1': 'refs/heads/test' exists; cannot create 'refs/heads/test/1'
Forward slashes are special in branch names; I'd guess because there's a filesystem representation of the branch name happening in there.
Is this on Windows? Or maybe you're on an older version of Git? I make branches with slashes in the name all the time, never had a problem with it.
The parent comment is specifically referring to creating a new branch from an existing branch with a slash in the name (ex: 'test-branch' and 'test-branch/new'). Trying it on my own machine (macos) yields an error. Even switching branches first and creating the new one yields an error.
First ran into this on persistent servers, where the deployed instance knew branch names github had long forgotten about.. Someone pushed a branch named "bugfix", and `git fetch` started erroring.

You get even _more_ interesting problems if part of your team has case-insensitive file-systems!

The version I have installed is a year old (2.39 is from last year) and this is a Mac rather than windows...

    % git init
    Initialized empty Git repository in /private/tmp/gittest/.git/
    % git commit --allow-empty -m "initial"
    [main (root-commit) fa2cf4e] initial
    % git checkout -b test
    Switched to a new branch 'test'
    % git checkout -b test/1
    fatal: cannot lock ref 'refs/heads/test/1': 'refs/heads/test' exists; cannot create 'refs/heads/test/1'
    % git --version
    git version 2.39.3 (Apple Git-145)
    % git checkout -b foo/bar
    Switched to a new branch 'foo/bar'
    % git checkout -b foo/qux
    Switched to a new branch 'foo/qux'
    % git checkout -b foo
    fatal: cannot lock ref 'refs/heads/foo':     'refs/heads/foo/bar' exists; cannot create 'refs/heads/foo'
The issue is that 'test' exists before 'test/1' was (attempted to be) created.

    % pwd          
    /tmp/gittest/.git/refs/heads
    % find . -print
    .
    ./test
    ./foo
    ./foo/qux
    ./foo/bar
    ./main
    % file *
    foo:  directory
    main: ASCII text
    test: ASCII text
In refs/heads, 'test' is a file and thus git is unable to create 'test/1'.
>I'd guess because there's a filesystem representation of the branch name happening in there.

Yep, it works exactly how you're assuming. You're asking the filesystem to create a directory with the same name as an existing file.

Por exemplo:

    ls .git/refs/heads
    cat .git/refs/heads/master
Interestingly, this prefix rule which is based on the filesystem-based ref storage is enforced even if refs are stored via alternative means like .git/packed-refs, since the refs could later be stored in the filesystem. You can verify this by calling `git pack-refs --all` and verifying that your .git/refs/heads directory is empty but you still cannot create conflicting branches.
A git branch is a pointer. It can point at whatever.

This shouldn't be confusing, but it is because that's not a natural way to think about revision control, so we instead have to relitigate it all the time for people that haven't learned git's obtuse naming conventions.

It seems like the natural way to me. What do you think the natural way is?
> It seems like the natural way to me. What do you think the natural way is?

A branch is a DAG of commits terminating at one commit.

They should have just called it a pointer if it's a pointer. But here we are anyway.

That’s not right. A branch is a single pointer to a single commit. Just open your .git folder. It’s then Git itself that follows that commit’s pointer to its parent(s).

The branch, like the tag, is a single reference to a commit.

Do this:

    $ cat .git/refs/heads/main
It seems accurate enough to say the branch includes all the parents of the pointed to commit. It's a matter of definition, but both definitions are technically accurate.

(Also, note the path you ask us to cat does not contain the word "branch". You refer to how things are stored to support your definition, but the way things are stored doesn't use the term "branch".)

The question was "what do you think is the most natural way to think about it", not "how does git represent it".
What? If you’re looking for “the most natural” then you’ll find it in the name. You can’t “explain” branch by using a synonym.

Plus that definition describes both tags and branches, how do you tell them apart? It’s clearly lacking. Actually that definition fits commits too.

I’d rather use these:

Branch: a pointer that advances

Tag: a permanent pointer

Commit/hash: an even more permanent pointer (because even a force can’t change it) that also describes the changes.

Technically, you're right. Practically however, it does make sense to implicitly dereference the branch you're talking about to the commit it points to. Asking "is this commit on that branch?" is semantically equivalent to "is this commit reachable from the commit that branch points to?".

That said, it only makes sense to make such implicit mental shortcuts once you're already comfortable with git's data model. If you're not and you're still learning it, it's only going to confuse you.

To be honest, describing a branch the wrong way is only going to confuse people more.

The “dots on the line” visualization is pretty clear: you can have any number of branches pointing to a specific dot/commit. How do you add “branches” without fundamentally breaking that concept? When you create a branch, you’re not copying the line, you’re just adding a pointer.

Are you saying that the "natural way" you think about branches is as a DAG terminating at one point?

Because that is true; that is accurate. Thus, I don't understand your original claim that branches work in an unintuitive way.

A branch in Git is just the pointer to the tip of the DAG. The way I find it natural to think about is it's the entire DAG of commits.

To put this in programming terms, a git branch is a pointer. I think of a branch as the value is the most natural way to think about it.

This mismatch is what leads to endless tutorials on what git actually means when it uses its vocabulary.

If I created my own git implementation with the exact same CLI, but under the hood I didn't use a "pointer", but instead used the "value", would this be more intuitive to you?

One problem I'm trying to express with that question is: You say it's unintuitive, but when asked why you refer to a very low level technical detail.

My suspicion is that people find git branches unintuitive, and thus they think something more intuitive must exist. They have vague ideas about what "more intuitive" might mean, usually it means "just do what I want in this one situation I'm currently facing". My theory is that, if pressed, nobody can fully define a behavior for branches that is more intuitive than the current git implementation.

Well, you're still using the same CLI which is part of the problem. Git's CLI reflects its patched-together-hacked-executable history.

It shouldn't be controversial to say the porcelain can and should be much more straightforward to use.

DVCS concepts like the DAG and where you want to apply diffs are complicated the first time around, but after you get the concepts it should be easy to use, not difficult.

Mercurial's branches are a better match for how I've seen branches use in practice. Tags are essentially the same between both. Evolve's hidden commits make it easier to recover lost data than Git's reflog, though the underlying implementation not permitting garbage collection kind of sucks.

> To put this in programming terms, a git branch is a pointer. I think of a branch as the value is the most natural way to think about it.

The branch name is a pointer. All pointers (commits) are also branches of the DAG, just anonymous. Orphaned commits (i.e., not reachable from any named branch or tag) can be garbage collected (deleted).

The point is that raw pointer values do not make for user-friendly interfaces. Humans deal well with names and poorly with numbers. Thus we need names. Every VCS in the world supports branch names. The issue here is that Git's default rules for branches are shockingly simple for users coming from Mercurial and other VCSes, but those default rules are also the simplest possible default rules and you can add rules on top.

Like you, I have no problem working with the raw pointer values some of the time, so I often work in detached-HEAD mode, though in the end I have to always name commits I don't want to lose to GC and that I want to be able to find in the future -- in the end I still need names for commits, I still need named branches and tags.

> A branch is a DAG of commits terminating at one commit.

[Pedantically] this includes orphaned branches.

> They should have just called it a pointer if it's a pointer.

A branch in every VCS is first and foremost a name. That name has to evaluate to a) content, b) history. In Git and Git-like immutable-commit systems this can trivially be implemented by a table mapping branch (and tag and...) names to commits. I.e., in any immutable-commit VCS branches are naturally pointers.

The only real question is whether there should be additional semantics layered upon the pointer nature of branches. Indeed, there should be. In Git these are the basic rules:

- branch names are updated when adding commits in a workspace that has that branch checked out

- tag names are NOT updated when adding commits when adding commits in a workspace that has that tag checked out

- local configuration can enforce a variety of rules via commit hooks

- upstreams can enforce a variety of rules, such as fast-forward-only (no history rewriting)

This gives users a great deal of flexibility, including the ability to ban history rewriting on published branches and to allow it on unpublished branches.

I think the most intuitive way to think about commits in a version control system is that they would be diffs. It's certainly how most people interact with git.

Even despite knowing that's not how they work in git, it's usually easier for me to treat them that way.

The appearance that commits are diffs is realistic enough though. The question was why branches (and tags, and other refs) as pointers is unnatural, and I second the question because for me it's quite natural, while Mercurial style branches feel quite restrictive (and I never internalized their rules).
The fact that it can point to anything seems a little bit confusing.

For example, when I was first learning about git, I decided for some silly reason to make a git repo to store all of my little experiments—20 lines of python or matlab type stuff—just to have them all in one place. The main branch basically just has a README to remind me why the directory is empty, and to look at the other branches.

This is a very silly way to use git, I think, because they aren’t really branches of one coherent project. Diffing and merging them is useless. If they produce temporary output files, they all do it in totally non-standard ways that have no relation to one another. There really is no value to collecting them together. But it is possible, of course, because git can point to anything! Thankfully it is hiding on my hard-drive, hopefully nobody will ever see it other than me.

That's more common than you think, because GitHub has the magic gh-pages branch which is used to publish to GitHub pages; that branch usually has no commits in common with the master branch.
Back when GitHub had a “maximum number of repositories” limit, and a project was done. We copied all commits from the done project to a branch on a “graveyard” repository. All branches were 100% unrelated to each other.
The problem is that we, both people using git and teaching git, constantly say thing like “a commit is on this branch” or “we need to merge these branches” or even in the OP zine “you can remove commits from a branch.”

All of these phrases reinforce the common misconception that branches are “containers” for commits.

It would seem that you could query, “oh, what branch is this commit on?” But that’s not how git works. A commit has no idea what branch it is on, and that’s unintuitive. The only definition of being “on a branch” is, starting at the commit that the branch is currently pointing to, all ancestors of that commit are on the branch.

I don't think it is inaccurate to say that a branch is a "container" for commits because a branch by definition points to a commit that has zero or more parent commits.

Having to walk the commit ancestry to see if a commit is on a branch doesn't make it not true.

It’s confusing because that pointer moves all over the place all the time in different ways. Sometimes the pointer moves when you perform other actions. Sometimes you want the pointer to move but it doesn’t.

The concept “a branch is a pointer” isn’t a confusing. However the practice of managing and predicting the behavior that pointer is complex, error prone, and full of nasty edge cases.

I can see how that is confusing at first glance, maybe.

I think people want branches to just do what they want. They complain that branches are confusing, but if you ask them to define how a branch should work, they won't have a complete answer.

Sure. They won’t have a complete answer BECAUSE ITS CONFUSING. If people could give a complete and detailed answer then they wouldn’t be confused, by strict definition!

There’s two problems here. One, branch tag behavior is genuinely confusing. Two, branch tags are intrinsically complex (which is why they’re confusing).

Git is intrinsically complex. Version control is a hard, complex problem! Pretending that it isn’t complex just results in a lot of confused users and a lot of unhelpful people who think if they yell “it’s actually simple” just a few more times everything will be ok.

The big question is, can we find a more intuitive interface?

I think git is pretty close to the most intuitive interface possible, but the problem git solves is hard, as you say. Even the best interface, programmed by God, might not make this easy.

Git is one of the worst interfaces possible. The underlying implementation is fine, but far too much of the plumbing is visible as porcelain.

Mercurial has about as intuitive an interface as is possible IMO. There's still all the intrinsic complexity of distributed version control, but the verbs are meaningful and if I don't know how to do something, knowing the verb is sufficient to use the built-in help to learn how to do the thing I want to do.

The concept of branches as the entire DAG and not just a commit also better matches how I've seen version control workflows be used in practice across multiple teams and companies. Add in rewriteable history, like what Git allows or Evolve enables, and you're golden.

In my day job we use trunk based development. There is no such thing as a branch. All commit labels are advanced exclusively by the CI system. All commits are rebased by CI.

It’s very simple to use and understand. It’s not perfect or anything. But Git is pretty damn far from intuitive imho. Especially for non-programmers who largely don’t use Git.

If you have a branch checked out and then add a commit, the branch had better "move" to point to the new commit, else you'd have to more work than merely `git commit` -- work that's easy to forget, leaving you with orphaned commits that can be garbage collected, and which can be hard to find so you can update the branch pointer.
>A git branch is a pointer. It can point at whatever.

Not really, a branch is only a pointer to a commit, not to trees or blobs.

The commit in turn points to 1 or more parents (unless its the root-commit) and to the tree object that represents the state of the workspace at the time commit and contains pointers to blobs (files) and other trees (subdirectories).

I think a lot of people drop any wrong information. It's a waste of time to think about wrong information.

This comic presents a scenario where someone believes wrong information: git branches have rules. Git branches do not in fact have rules. This is wrong information. Why would we talk about wrong information?

We talk about wrong information because other people do not have up to date and correct information. We can try to just update their information to be correct, but that does not always work. In some cases they have built a structure of reasoning based on their belief. It is traumatic to destroy this structure of reasoning without providing a replacement.

Thus it can be helpful to understand the bad reasoning (based on wrong information) in order to communicate more effectively.

Git branches do have rules. The most basic rule is that if you have a branch checked out and you add a commit, then the branch name moves. Upstreams can also have rules like "no history rewriting".
Thank you for more correct information.
Git ‘remotes’ can have rules (hooks that run on push, and decline).

GitHub has ‘branch protection’ which prevents various changes. But GitHub is not Git. A common misconception lately due to its popularity.

Git itself is nothing more than a set of tools to manage the versions of a repository. How you choose to use it is up to you, and what you allow.

Solo developer? Rebase, or don’t.

Do you collaborate? Maybe consider using a SaaS product like GitHub and setup your rules.

> Git itself is nothing more than a set of tools to manage the versions of a repository.

"Git" is also the protocol, not just the implementation. Indeed, here I'm referring to the Git protocol more than to the Git implementation. There are multiple implementations of the Git protocol. GitHub is but one of several.

> But GitHub is not Git.

To the client it's indistinguishable.

Git branches have rules on origin - those rules are enforced with a big stick.

What you do in your own clone of the repository is between you and god alone.

(comment deleted)
The reflog is not always there. By default git gc only keeps the last 90 days.
Another rule that git branches do not have: they don't have to have originally come from the same repo. You can pull a branch from a different project if you want to. (This is useful if you are trying to combine two repos while preserving history.)
With respect, the person doesn't understand the purpose of git.

Doing 'git pull' often would fit for a very specific workflow, and one that in time proves to be a wrong workflow, in my opinion.

Don't do things that don't have a conscious purpose.

You are commenting on one page of a webzine, written by a person who has used git for years and has written lots of educational material on git and other technical subjects. She understands the purpose of git.

In the linked comic she is pointing out, correctly, that while there are conventions that make good workflows, git doesn't enforce them and allows you to do stupid stuff. Surely you don't disagree with that.

Git is a distributed system, meaning that you're supposed to have different histories.

Git gives you the tools to allow any member of the team to reconcile those histories cleanly for others.

Forcing everyone to be in sync all the time also means lots of merge conflicts.

Git allows for everyone to drift in their own way and provides tools for them to reconcile that drift in a way that others can integrate easily.

Until you embrace the distributed nature and start using tools to manage your history (rebase & reset) , you will suffer with git.

I cringe when editors refer to "rebase & reset" as "weird".

Rebase and reset are the primary power tools of git. Along with reflog, revert, interactive-add .

The worst repositories I've seen are people who `git merge` 20x a day to stay "in sync" and create labyrinthian change logs that are impossible to parse.

Paying the cost of a distributed system for a centralized team of 10 is a horrible idea. Better for someone on the team to teach everyone else a simple set of rules (git checkout -b before starting a new featire, git pull --rebase origin to keep up to date, git push origin when you're done) and pretend Git is a centralized system as much as possible. You're anyway going to have a central server which is the source of truth. If your local copy goes bad, there's always rm -rf ./ && git clone. No reason to pretend your local history has some value.

Of course, if you are working on a highly distributed project, like the Linux kernel, many of the Git tools are absolutely invaluable. But there's no reason to bother with them if you're not.

I like your idea of feature branches, but people do need to know the data model and how to manage their history. They will inevitably be out of sync and clumsily tring to get merge all day to get their files in sync.

Keeping the history clean is more important than just syncing files.

This seems like a reasonable take - there's only one problem: you haven't told us how to draw the rest of the owl.

How does one learn how to (1) think in this way (which necessitates not only abstract description but multiple concrete examples) and (2) what workflows and commands are necessary to effectively implement this?

(comment deleted)
the team has 2 deliverables:

1. a ref (branch or tag) that builds (compiles & passes all tests including UAT)

2. every shared ref has a clean revision history that allows for graceful change management ( read changelog, fork, integrate, revert, A/B test)

Any ref shared by a team member (e.g. git push) must meet those two qualities. Team members must reconcile refs on their end (e.g. rebase) before they can submit a new ref meeting those expectations.

This way you are not prescribing a workflow (e.g. git-flow). The team needs to agree on that to their software delivery pipeline requirements.

In order to deliver these two expectations, they need to understand refs (pointers to a digraph) and how to manage them (rebase). tell them to RTFM the git manual on those topics.

Just like memory management, pointers, algos etc these skills are essential . They are not a "nice to have" to build software.

Merging is a process that I like over rebasing exactly because it conserves the history.

With a repository server that supports merge requests and feedback from CI/CD tooling on commits/push you can motivate people to use merges rather cleanly.

If you add to that very simple rules like

- create branches for every feature

- commit often

- push often

- merge only via merge/pull requests

then even people new to Git can handle it just fine.

Merge conflicts happen when multiple people work on the same file and try to merge their different status. Imho, code bases where multiple people work on the same file at the same time have a more fundamental problem than Git.

Honestly, I still struggle to see the value in rebasing. Maybe because I haven't used it extensively. Superficially, to me at least, it does the same as a "git push -f".

I am curious to hear why it is supposed to be superior, especially with modern CI tools like Github and the likes.

Merge isn't superior to rebase; they're both tools with different purposes.

Have you ever squashed down a branch before merging back into main? Then you've used rebase.

The purpose of source control (not just git) is to keep a useful version history of your project. If you don't need to know where/when a commit originally branched off from, rebase can keep a cleaner history than merge.

Even rebasing a branch off the latest commit just to merge back in can be useful.

I never use squashing, because the history would not be easily accessible afterwards.

In popular Git ecosystems like Github you have the option to add a merge commit. To me, this also helps understanding the history of the repo. Would the rebasing be equally visible?

Rebasing is usually (but not exclusively) useful before pushing to the shared repo, at a point when there's no proper history being added to it yet, so you can put your changes into shape before actually publishing them. Learning how to effectively use rebase and its all goodies is one of the most useful things that git has to offer and almost transforms the way you work with version control, as with it you're able to utilize its power in more areas than if you only ever commit and push. For me, git is not just a collaboration tool - it's also an "undo" log on steroids; something that extends my text editor and lets me manage the changes regardless of whether they're meant to ever leave my PC or not.

Git is a powerful data structure manipulation tool. Use it as such and harvest that power. After all, what you're working on is just a graph of repository states - and you can attach any meaning you find useful to those states.

Rebase can split commits as well as combine them. Rebase can be used to improve the commit messages of previous commits, etc.

If the history is important, as you assert, then a tool for rewriting and improving the history is also important.

Arguably, it is not the real history anymore if you make it prettier.

Do you put manual work into rebases? It sounds like a lot of effort and time when collaborating.

It's not the full history, you're right. But nothing in git will ever be the full history.
git merge doesn't conserve the history it pollutes it. any more than a few merges or a few developers and you are left with an impossible to read merge log with 10+ parallel paths.

Git rebase preserves a linear history . Keep a reflog on the server (github has one built in) for cases where someone has screwed up the history. and secure shared branches to avoid clobbering it.

Yes, securing branches is very important, much like git-hooks if you are serious about the Git flow.

How do you specifically investigate the history with rebase and 10+ developers? Do you manually look into commits on the branch, or go by a single file history, or look at merge/pull requests?

Personally, I prefer looking at the context of a file change within a merge/pull request. The specific merged/pull request can be read from the merge commits. And in case a specific file history is of interest, I can backtrack from that file's associated commits.

if master has a clean history, then use git-blame on the file to see where a change was introduced, and git show to see the entire change. then you can walk back through the git log to understand the context of the change.
Merge doesn't pollute the repository. Just like rebase, it's a valid tool to use when what you want to do with your repo structure calls for it. It's the fact that most people use git blindly and end up doing things that don't make sense that pollutes their histories.

There are tons of merge commits in the Linux repository, but no useless ones. Note how in its nature Linux process also does implicit rebasing of applied patches when they land in subtrees.

There is more than one way to do it. If that workflow works for you, fine.

> I still struggle to see the value in rebasing. Maybe because I haven't used it extensively. Superficially, to me at least, it does the same as a "git push -f".

Rebasing is completely different from pushing. Rebasing changes your history. You can do whatever you want with your history: rebasing, creating personal branches and moving them around, etc... and no one will know. Only your pushes matter to the rest of the world, or alternatively when others pull from your repository. "git push -f" means you don't want to play along with whatever is in the repository you are pushing into, it is usually bad as it may desynchronize it from everyone else's fork. You absolutely don't need to do a "git push -f" with rebase, in fact you really shouldn't.

And none of your rules prevent rebasing. The commit/push often rule make the need for rebasing less likely, but if a dev did a rebase, no one will notice, it didn't leave his own machine. He mostly did that to make the commit he pushed less awkward.

(comment deleted)
This is not a problem. I rewrite history all the time, locally (where locally means "unpublished" rather than "not pushed anywhere"). This is a feature.

Upstream branches can and do have rules, like "fast-forward only pushes", which means "no history rewriting here (because this branch is published)".