Skip to content

Workflow: Divergent development

kain88-de edited this page May 4, 2024 · 67 revisions

This article is a rough draft. Feel free to open a Discussion regarding the material.

Description

"Divergent development" is what I call a certain workflow which involves making frequent speculative commits and often backtracking and trying a different approach. In this sense, development "diverges" frequently.

Divergent development is especially useful when a problem is still vague and you don't know exactly what changes you want to make and commit. You may make several prototypes and throw away most of them.

It has fewer benefits when you know ahead of time what you intend to do. In those cases, a rigorous approach like writing your commit messages first or practicing Test-Driven Development may be more appropriate.

Stock Git alternatives

Compared to branching approaches

You can use branches with stock Git to achieve the same workflow. However, there's a lot more friction:

  • At each branching point, you have to make and name a new branch, as well as decide on a commit message. Why do you need to both?
  • You have to always make sure that the correct branch is checked out when making a new commit.
  • Stock Git does not have good ways of rebasing a sequence of branches. However, git-branchless's git move command makes this easy.
  • Stock Git does not have a good way of amending a commit which has descendant commits, especially if it has multiple descendant branches. However, git-branchless's git restack command makes this easy.

Compared to stashing approaches

You can use git stash to backtrack and set aside your current work. Under git-branchless, it's better to use commits and never use stashes (see Workflow: Stashing):

  • It's easy to forget about old stashes. With git-branchless, speculative commits will appear in the smartlog, and can't be forgotten about.
  • It's not clear which commits an old stash can be applied to. With git-branchless, the parent of a given commit appears in the smartlog. You can also move it somewhere else in the graph with git move, without having to check it out. Merge conflict resolution will not be initiated unless specifically requested, so you don't have to worry about cancelling a merge.
  • You can't easily encode a tree structure or dependent stashes in the list of stashes.

Compared to staging approaches

You can use the staging area to manage a set of speculative changes. However, it has some shortcomings when used for this purpose:

  • You can accidentally lose work in the staging area with an unfortunate Git command that happens to touch the staging area.
  • You can have at most one set of staged changes.
  • It might be difficult to move the set of staged changes to a different location in the commit graph.

Under git-branchless, it's recommended to use the staging area only to make partial commits, or not at all.

Approach

Make changes

1. First, detach any branch you might have checked out:

$ git checkout --detach

2. Make a speculative change and commit it. It does not need to build or pass tests!

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 53s A
┃
◯ 666b4b9e 53s B
┃
● 3a05375e 53s (feature) C

...do work...
$ git commit -m 'temp: speculative change 1'

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 1m A
┃
◯ 666b4b9e 1m B
┃
◯ 3a05375e 1m (feature) C
┃
● 28baf953 2s temp: speculative change 1

3. Continue making changes. Make frequent commits as backups, in case you want to refer to an old approach. We will clean up the excess commits later.

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 3m A
┃
◯ 666b4b9e 3m B
┃
◯ 3a05375e 3m (feature) C
┃
◯ 28baf953 2m temp: speculative change 1
┃
◯ 2dfed453 9s temp: speculative change 2
┃
● 882b958c 7s temp: speculative change 3

Backtrack as necessary

1. If you decide you want to try a different approach, then backtrack. Commit any work you currently have (even if it doesn't build!), then run git prev to return to a previous commit in the stack (or git checkout with a specific commit):

$ git prev 2
$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 5m A
┃
◯ 666b4b9e 5m B
┃
◯ 3a05375e 5m (feature) C
┃
● 28baf953 4m temp: speculative change 1
┃
◯ 2dfed453 1m temp: speculative change 2
┃
◯ 882b958c 1m temp: speculative change 3

2. Make more speculative changes from this starting point as necessary.

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 6m A
┃
◯ 666b4b9e 6m B
┃
◯ 3a05375e 6m (feature) C
┃
◯ 28baf953 5m temp: speculative change 1
┣━┓
┃ ◯ 2dfed453 2m temp: speculative change 2
┃ ┃
┃ ◯ 882b958c 2m temp: speculative change 3
┃
◯ 47bf1c3e 3s temp: speculative change 4
┃
● 551c36df 1s temp: speculative change 5

Clean up old commits

When you've decided on an approach to keep, clean up any old commits with git hide:

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 8m A
┃
◯ 666b4b9e 8m B
┃
◯ 3a05375e 8m (feature) C
┃
◯ 28baf953 7m temp: speculative change 1
┣━┓
┃ ◯ 2dfed453 4m temp: speculative change 2
┃ ┃
┃ ◯ 882b958c 4m temp: speculative change 3
┃
◯ 47bf1c3e 2m temp: speculative change 4
┃
● 551c36df 1m temp: speculative change 5

$ git hide -r 2dfed453
Hid commit: 2dfed453 temp: speculative change 2
To unhide this commit, run: git unhide 2dfed453
Hid commit: 882b958c temp: speculative change 3
To unhide this commit, run: git unhide 882b958c

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 8m A
┃
◯ 666b4b9e 8m B
┃
◯ 3a05375e 8m (feature) C
┃
◯ 28baf953 7m temp: speculative change 1
┃
◯ 47bf1c3e 2m temp: speculative change 4
┃
● 551c36df 2m temp: speculative change 5

Remember, if you want to get those commits back, you can use git undo.

Confirm the new work

First, decide whether you want to make a new commit or combine these changes into a previous commit. We'll use an interactive rebase to edit the commit graph appropriately. Remember that you can use git undo to revert the changes to your commit graph, if necessary.

Run an interactive rebase, such as with git rebase -i main. To make a new commit, reword a commit message for the first commit in the sequence, and fixup the remainder of the commits. (You can also use squash to accomplish this.)

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 13m A
┃
◯ 666b4b9e 13m B
┃
◯ 3a05375e 13m (feature) C
┃
◯ 28baf953 12m temp: speculative change 1
┃
◯ 47bf1c3e 7m temp: speculative change 4
┃
● 551c36df 7m temp: speculative change 5

$ git rebase -i main
...editor opens...

pick b91eec5 A
pick 666b4b9 B
pick 3a05375 C
reword 28baf95 temp: speculative change 1
fixup 47bf1c3 temp: speculative change 4
fixup 551c36d temp: speculative change 5

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 13m A
┃
◯ 666b4b9e 13m B
┃
◯ 3a05375e 13m (feature) C
┃
● 5af31cdb 2s My new commit

Exit the editor and edit the commit message as appropriate.

If instead you want to combine these changes into the previous commit, use fixup for all of the commits:

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 13m A
┃
◯ 666b4b9e 13m B
┃
◯ 3a05375e 13m (feature) C
┃
◯ 28baf953 12m temp: speculative change 1
┃
◯ 47bf1c3e 7m temp: speculative change 4
┃
● 551c36df 7m temp: speculative change 5

$ git rebase -i main
...editor opens...

pick b91eec5 A
pick 666b4b9 B
pick 3a05375 C
fixup 28baf95 temp: speculative change 1
fixup 47bf1c3 temp: speculative change 4
fixup 551c36d temp: speculative change 5

...
branchless: This operation abandoned 1 branch (feature)!
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
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 14m A
┃
◯ 666b4b9e 14m B
┣━┓
┃ ✕ 3a05375e 14m (rewritten as 6a4862ff) (feature) C
┃
● 6a4862ff 8s C

For some interactive rebase operations, a warning telling you that commits have been abandoned may be produced. You typically want to run git restack in those cases to fix the commit graph:

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 16m A
┃
◯ 666b4b9e 16m B
┣━┓
┃ ✕ 3a05375e 16m (rewritten as 6a4862ff) (feature) C
┃
● 6a4862ff 1m C

$ git restack
...

$ git sl
⋮
◇ e6adfe90 9d (main) Main branch commit
┃
◯ b91eec5c 16m A
┃
◯ 666b4b9e 16m B
┃
● 6a4862ff 1m (feature) C

Create a new branch

In order to upload to GitHub or similar, you may need to create a branch for your work. Some approaches:

  • Run git branch <name> <commit> to create branch <name> pointing to <commit>.
  • Run git branch <name> to create branch <name> pointing to the current commit.
  • Run git switch --create <name> <commit> to create branch <name> pointing to <commit> and also switch to it.
  • Run git switch --create <name> to create branch <name> pointing to the current commit and also switch to it.
  • Run git switch main && git merge - to switch to main and merge the previous commit into it.
  • Run git sync 'stack()' to rebase this commit stack onto the main branch.

See also Convenient way to move branch pointer around?.

Clone this wiki locally