1. 24
  1.  

  2. 9

    This workflow is just bringing (client-side) Git up to parity with Mercurial as used at Google/Facebook:

    • Adds in-memory rebases (for performance reasons).
    • Adds changeset evolution.
    • Encourages of a “patch-stack”/“stacked-diff” workflow with trunk-based development.
    • Discourages the use of branches/stashes/staged changes, in favor of commits for everything (where possible).

    Unfortunately, I’ve found it hard to motivate the collection of features as a whole. But most people can use at least one of the following in their workflow:

    • git undo to undo operations on the commit graph.
    • git move as a saner, faster replacement for git rebase.
    1. 3

      I have looked at this project a few weeks ago and I seem to not get it. Maybe it is something with the naming, but how is it branchless? All work in git is on a branch. What is the difference if I have a local checkout and work on feature-xyz instead of main? I still do git fetch && git rebase origin/main regardless of the local branch name. I could not figure it out from the README tbh.

      Note that I am from the “rebase, squash and “no-merge-commits” school of thought, so maybe this is going in a similar direction, but I don’t really understand what the usp here is. Maybe it makes more sense to people who have used mercurial?

      1. 11

        All work in git is on a branch.

        Under git-branchless, this is no longer necessary. There are three main ways to not use a branch:

        • Run git checkout --detach explicitly.
        • Use one of git next/git prev to move along a stack.
        • Use git checkout with a commit hash (or git co from the latest source build).

        Why is this useful? It helps you make experimental changes to various parts of the commit graph without dealing with the overhead of branch management. Suppose I have a feature feature made of three commits, and I get feedback on the first commit and want to try out various approaches to addressing it. My session might look like this:

        $ git checkout -b feature
        $ git commit -m A
        $ git commit -m B
        $ git commit -m C
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ◯ 9775f9a6 4m A
        ┃
        ◯ 97b8d332 4m B
        ┃
        ● 67fa26af 4m (feature) C
        
        $ git prev 2
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ● 9775f9a6 6m A
        ┃
        ◯ 97b8d332 6m B
        ┃
        ◯ 67fa26af 6m (feature) C
        
        $ git commit -m 'temp: try approach 1'
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ◯ 9775f9a6 7m A
        ┣━┓
        ┃ ◯ 97b8d332 7m B
        ┃ ┃
        ┃ ◯ 67fa26af 7m (feature) C
        ┃
        ● 9dba147f 1s temp: try approach 1
        
        $ git prev
        $ git commit -m 'temp: try approach 2'
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ◯ 9775f9a6 8m A
        ┣━┓
        ┃ ◯ 97b8d332 8m B
        ┃ ┃
        ┃ ◯ 67fa26af 7m (feature) C
        ┣━┓
        ┃ ◯ 9dba147f 58s temp: try approach 1
        ┃
        ● ef9ea69a 1s temp: try approach 2
        
        # Check out "temp: try approach 1".
        # Note that there is no branch attached to it,
        # so we use the commit hash directly.
        # (Or we can use the interactive commit selector
        # with `git co`.)
        $ git checkout 9dba147f
        $ git commit -m 'temp: more approach 1'
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ◯ 9775f9a6 10m A
        ┣━┓
        ┃ ◯ 97b8d332 10m B
        ┃ ┃
        ┃ ◯ 67fa26af 10m (feature) C
        ┣━┓
        ┃ ◯ 9dba147f 3m temp: try approach 1
        ┃ ┃
        ┃ ● 60ad3014 4s temp: more approach 1
        ┃
        ◯ ef9ea69a 2m temp: try approach 2
        
        # Settle on approach 1.
        # Hide "temp: try approach 2" from the smartlog.
        $ git hide ef9ea69a
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ◯ 9775f9a6 11m A
        ┣━┓
        ┃ ◯ 97b8d332 11m B
        ┃ ┃
        ┃ ◯ 67fa26af 11m (feature) C
        ┃
        ◯ 9dba147f 4m temp: try approach 1
        ┃
        ● 60ad3014 1m temp: more approach 1
        
        # Squash approach 1 into `main`:
        $ git rebase -i main
        ...
        branchless: This operation abandoned 1 commit!
        branchless: Consider running one of the following:
        branchless:   - git restack: re-apply the abandoned commits/branches
        branchless:     (this is most likely what you want to do)
        branchless:   - git smartlog: assess the situation
        branchless:   - git hide [<commit>...]: hide the commits from the smartlog
        branchless:   - git undo: undo the operation
        branchless:   - git config branchless.restack.warnAbandoned false: suppress this message
        Successfully rebased and updated detached HEAD.
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┣━┓
        ┃ ✕ 9775f9a6 12m (rewritten as 14450270) A
        ┃ ┃
        ┃ ◯ 97b8d332 12m B
        ┃ ┃
        ┃ ◯ 67fa26af 12m (feature) C
        ┃
        ● 14450270 32s A
        
        # Move B and C and branch "feature" on top of A,
        # where they belong
        $ git restack
        $ git sl
        ⋮
        ◇ fcba6182 7d (main) Create foo
        ┃
        ● 14450270 1m A
        ┃
        ◯ 7d592eae 3s B
        ┃
        ◯ fec8f0ba 3s (feature) C
        

        So the value for the above workflow is:

        • No thinking about branch names, especially since they’re ephemeral and going to be deleted shortly anyways.
          • Some people don’t find this an impediment to their workflow anyways.
        • Automatic fix up of descendant commits and branches.
          • Works even if there are multiple descendant branches (git rebase moves at most one branch).
          • Works even if one of the descendants has multiple children, causing a tree structure.
        1. 1

          Maybe a dumb question, is there an equivalent to hg heads in git-branchless?

          1. 1

            Not yet, but it should be pretty easy as part of the task to add revset support (https://github.com/arxanas/git-branchless/issues/175).

          2. 1

            Thanks for the explanation! This is very different from the git I use. I don’t think I ever had a use case for this sort of workflow, but I can see how people might find it useful. I will keep a bookmark and revisit this again in the future. Maybe it turns out useful.

        2. 3

          Seems like this workflow tries to avoid interactive rebase (and other tools are recommended to do that in-memory). I… can’t exactly imagine not using it all the time. Especially in situations like maintaining a constantly rebased “patchset” on top of an upstream, occasionally submitting patches to it. In my mind “restack” means “reorder in interactive rebase”, not what the restack tool does.

          What does changeset evolution mean btw?

          1. 2

            It’s not that the workflow tries to avoid interactive rebase, but that it simply doesn’t offer a good replacement for interactive rebase at present. (It should handle in-memory rebasing, rebasing/editing tree structures, and referencing commits without branches.) It’s on the roadmap: cl https://github.com/arxanas/git-branchless/issues/177

            Changeset evolution is this feature from Mercurial: https://www.mercurial-scm.org/wiki/ChangesetEvolution. In short, it tracks the history of a commit/patch as it gets rewritten with commands like git commit --amend or git rebase (or their equivalents in git-branchless). It means that we can automatically recover from situations where descendant commits are “abandoned”:

            $ git commit -m A
            $ git commit -m B
            $ git commit -m C
            $ git prev 2  # same as git checkout HEAD^^
            $ git commit --amend -m A2
            # now commits B and C are based on A instead of A2,
            # which is probably not what you intended
            $ git restack  # moves B and C onto A2
            
          2. 1

            I’ve been interested in this toolkit since I first came across it but I’m reluctant to set up rust on my laptop to install it. Do you have any enthusiasm for packaging releases yourself?

            1. 1

              Maybe, I haven’t looked into it too much. What platform are you using? Some kind folks have already packaged it for Nix, if you can use that.

              1. 2

                macOS for the most part. Mac users as a whole would probably most readily try this out if it were in Homebrew. (I have a bit of a grudge against Homebrew for being slow and pulling in outlandish dependency closures with stuff so I tend to prefer graphical installers or copying things into ~/bin when those are provided options.)

                (Really if I had ever set up Rust for something else on my work computer I’d probably be trying it out as is instead of griping but y’know.)

                1. 2

                  Something like https://github.com/emk/rust-musl-builder makes building static binaries very easy, if you want to go that way.

            2. 1

              git rebase -i can only repair linear series of commits, not trees. If you modify a commit with multiple children, then you have to be sure to rebase all of the other children commits appropriately.

              Note: git rebase -ir can rebase merges. Admittedly this requires some advanced git experience.

              1. 2

                For the unfamiliar reader, there are two interesting cases to consider here: rebasing a parent that has multiple children, and rebasing a child that has multiple parents.

                git rebase with the -r/--rebase-merges option will automatically generate a plan to rebase merge commits (a child with multiple parents). But if you want to rebase children that have multiple commits, then you have to laboriously write/edit the plan yourself. For the on-disk scenario, git move will actually generate a plan with the laborious label/reset commands, which git rebase -i can then execute.

                1. 1

                  Oh oops, you’re correct, I misread the quote.