Coder Social home page Coder Social logo

clean-push's Introduction

clean-push

Automated git flow to produce safe, neat (rebased + squashed) pull-requests.

Have you ever been frustrated with git because:

  • Your long-lived work branch commit-history has become too messy when the time has come to publish your changes?
  • You had duplicate or partly overlapping commits because of history-changing squashes & rebases?
  • A commit has been reverted, but instead of having an empty delta you had both commit + reverted changes?
  • When you tried to rebase, you got into "rebase hell" (continue, skip, abort) loop, which seemed to never end?
  • Syncing your repo with origin/master created more conflicts than expected?
  • A push after a merge unexpectedly included someone-else's changes due to a messy merge with master + a 3-way diff against an already existing PR?
  • You wanted to reuse an existing branch for additional work, but there was too much legacy in its commits so you were forced to start a new one and replay all your changes again?
  • You resorted to (git diff + git apply) or (stash + pop) or (a loop of git cherry-picks from prior work) but found the multi-step process too complex/involved and error-prone?
  • You created a pull-request, and noticed a small error, now you want to redo the PR with some small fix, but hesitate because it would mean too much work all over again? (think git commit --amend but for pushes)

If you answered "yes" to any of the above?

clean-push may be just the script you need.

clean-push implements the following:

  • A full sync with the main branch before starting a push leading to a pull request
  • Simple conflict detection, like merge does
  • Consolidated/simple conflict resolution (unlike with git rebase)
  • The pull-request is clean:
    • It has only one delta (diff) vs main
    • It is rebased on top of the latest HEAD of main
    • It doesn't contain unwanted merge commits
  • For safety: most risky git operations which might mess-up your work, are done on a temporary/throwaway branch.
  • Your feature branch is atomically modified once (with one git reset) after all issues have been resolved and the single delta vs main looks good.
  • You get a chance to edit your pull-request message in your favorite editor and make it look even better.
  • You can re-edit all the messages in a long list of commits without the complexities introduced by the fix-up syntax of git rebase -i
  • The pull-request on github looks exactly like you wrote it in your editor: the first line becomes the title of the pull-request
  • If you want to fix-up anything, you can just repeat the call to clean-push and the new push will override the previous instead of appending to it
  • You never get into "rebase-loop hell", because git rebase isn't used anywhere
  • Works both on Linux and Mac OS-X
  • Protected from being called from a git hook (a nested call which may cause damage)
  • Ability to pause & allow you to edit intermediate git steps before executing them. A common use-case for me is to be able to add --no-verify to the end of a git commit or git push sub-command in order to skip some long duration hooks.

Crucial terminology

In this text, there are repeated references to two distinct branches.

The 1st is the main branch you have branched from to do your development. This branch can be referred to by several names, among them:

  • master
  • main
  • parent branch (although git doesn't really support parent/child relationships between branches, as branch names are just refs, i.e aliases for commit-ids)
  • CICD branch (if we auto-deploy from it)
  • dev or the 'development branch'
  • The source of truth
  • When this branch is used to release from, it may be called the release branch
  • On github.com in settings/branches they call this the "default branch"

Similarly, different people may refer to the ephemeral branch they're developing on, as any of:

  • work branch
  • topic branch
  • bug-fix branch
  • feature branch

As far as clean-push is concerned, there are only two branches. In this text, they are referred to as:

  • main or master
  • work branch

Regardless of what they are actually called.

clean-push queries the current branch (from which it was called) in runtime for the work branch actual name.

Figuring-out the main branch actual name is harder. clean-push tries to check the following for existence, in order:

  • "$1" (first, arg, if passed on the command line)
  • dev
  • staging
  • main
  • master

Better heuristics for figuring out the later are welcome (please open a github issue if you know a more reliable and elegant solution).

Usage:

While working on your work/topic branch, you can run any of:

    clean-push [<main-branch-name>]

    4-way-diff [<main-branch-name>]

The argument is optional. If you don't provide it the script(s) will try to guess it. See also the Customizing behavior section below.

4-way-diff

4-way-diff is a handy script that provides a quick, non-destructive, (read-only) view of the 4-way state for full situational awareness.

It tells you which of the 4 copies is not in-sync by performing the full circle of comparisons:

  • local work branch vs its 'git index' (is current branch fully committed a.k.a: clean?)
  • local work branch vs remote/pushed work branch
  • local work branch vs remote master
  • remote work branch vs local copy of master (the 'tracking' branch for master)

Try it and it can help you to quickly diagnose what may still not be in-sync.

Here's for example the message you get when something isn't committed yet:

[full diff comes here]

4-way-diff: (1) work branch not clean. Need to commit (or stash) locally

Here's the message you get when your branch is fully committed but has not been pushed yet:

[full (reverse) diff comes here]

4-way-diff: (2) work branch: local != remote. Need to push

Here's the message you get when your branch changes are committed, pushed, but not yet merged to the main branch on the remote server:

[full (reverse) diff comes here]

4-way-diff: (3) local work != remote main. Need to remote-merge

Here's the message you get when your branch changes are committed, pushed, and merged remotely, but your local main branch is now a step behind (because the merge was only done on the remote):

[full diff comes here]

4-way-diff: (4) local work != local main. Need to pull in local main
To fix:
        git checkout main && git pull && git checkout work

And finally, the message you get when everything is in-sync:

(1) work branch is clean, cool
(2) work branch: local == remote, no need to push, cool
(3) remote work doesn't exist (already merged?)
(4) local work == local main, no need to pull in main, cool

Customizing behavior

The following default behaviors can be changed in both clean-push and 4-way-diff:

  1. Name of "upstream" (git remote repository location)

  2. Names of "main" branch-names to try

  3. If your remote is not called origin, you may change the variable you should change the default value of the UPSTREAM variable in the code, from:

UPSTREAM=${UPSTREAM:-origin}

to:

UPSTREAM=${UPSTREAM:-<your_upstream_name>}
  1. In order to figure out the "main" branch from which you started and want to merge into, the code makes a few possible guesses.

If these values don't work for you, you may change the value of MAIN_BRANCH_NAMES=( ... ) in the code.

You may also pass an explicit "main" branch name argument to any of the scripts which will disable the guessing altogether, e.g.:

    # Run a clean-push vs 'cicd-branch'
    clean-push cicd-branch

HOWTO

First, make sure the two utilities:

clean-push
4-way-diff

are always in your $PATH. I simply have ~/bin in my $PATH and have them both copied there, like this:

mkdir -p ~/bin

# Make sure the two scripts are executable:
chmod a+rx clean-push 4-way-diff

cp clean-push 4-way-diff ~/bin

# Make sure you have ~/bin in your PATH.
# This has to be in your ~/.bashrc or ~/.profile
export PATH=~/bin:$PATH

Now go to your repo (cd .../your/repository) Assuming you start in branch 'main':

# Create the 'work' branch.
git checkout -b mydev

# Add/remove files and make changes as you would normally do in git...

# Once: ready to push, make sure everything is committed:
git commit -a

# Now that your repo is 'clean', run the clean push
clean-push

Similarly, to compare to remote, just run:

4-way-diff

That's all there is to it!

Caveats

clean-push is intended for flows:

  • With a single common main branch (main, master, or similar).
  • Where developers want to be in sync with each other as much as possible
  • Where the main branch is the source of truth (e.g. used for continuous/automatic CICD and releases)
  • Encourages fast-development by many developers on the main branch
  • Detects conflicts as early as possible by frequent merging for other developer branches

If your flow:

  • Encourages multiple long-lived diverging separate development tracks
  • Rarely merges
  • Has multiple forks like production vs "next gen". Extreme case: what you develop today will be seen by customers only in 3 years, if at all.
  • Doesn't use continuous integration and deployment (CICD)

Then clean-push may be useful for work on one fork, but may not be useful beyond it.

Credits:

clean-push is based on method (1) of this excellent page by Lars Kellogg-Stedman

Before discovering that page, I tried several unsatisfactory solutions to the problem, all of which fell short on some aspect of the problems described above.

Thanks to Steve Malmskog & Jordan Bucholtz for their early testing & contributions.

Portability notes

clean-push is a simple bash script.

It was developed and tested using:

  • bash: 4.4.20, 5.0.17
  • git: 2.17, 2.25.1

Verified on Linux (Ubuntu 18.04, and 20.04) & Mac OS-X (10.15, Catalina).

License

Written-by: Ariel Faigon, 2021

Released under the MIT licence.

clean-push's People

Contributors

arielf avatar j-bucholtz avatar stevemns avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

clean-push's Issues

assumes origin

hi,
nice script, but it seems to assume remotes are at 'origin'. in my case I'm commonly using 'upstream' for example. it would be great if I could pass that alternative in somewhere.
A few actual usage examples in the readme would also be v helpful instead of needing to read the script to see what the options are.

Cheers.

no-diff (nothing added to the commit) flow can be improved

Example session where someone else merged a delta into main and my local dev already had that delta.
In this case we detect this a bit late (while on the temp-branch doing the integration) and when we fail we remain there.

% ./clean-push  
./clean-push: cool: repo is clean (fully committed)
Main branch:      main
Feature branch:   dev
Temporary branch: temp-branch
-----------------------------------------------------------------
(1) Pull latest remote 'main' into local branches (main & dev)
    (conflicts may be detected)
$ pull-main-branch-into main && pull-main-branch-into dev
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 5 (delta 2), reused 3 (delta 2), pack-reused 0
Unpacking objects: 100% (5/5), done.
From ssh://github.com/arielf/clean-push
 * branch            main       -> FETCH_HEAD
   0328273..9e3e8cc  main       -> origin/main
Updating 0328273..9e3e8cc
Fast-forward
 4-way-diff | 3 +++
 clean-push | 9 +++++++--
 2 files changed, 10 insertions(+), 2 deletions(-)
Switched to branch 'dev'
Your branch is up to date with 'origin/dev'.
From ssh://github.com/arielf/clean-push
 * branch            main       -> FETCH_HEAD
Updating fc6ab3f..9e3e8cc
Fast-forward
-----------------------------------------------------------------
(2) Create temporary work branch from 'main' -> 'temp-branch'
$ git checkout -b temp-branch main
Switched to a new branch 'temp-branch'
-----------------------------------------------------------------
(3) Merge + Squash delta from 'dev' (on pristine temp-branch branch)
    (conflicts may be detected)
$ git merge --squash dev
 (nothing to squash)Already up to date.
-----------------------------------------------------------------
(4) Commit on 'temp-branch': please edit/clean the commit message in your editor

    Note: the final form of this commit message is _exactly_ how it
    will appear in the pushed PR. First line will be subject/heading.
$ git commit

On branch temp-branch
Untracked files:
        tryme

nothing added to commit but untracked files present
./clean-push: git commit FAILED: status=1

If we could detect the empty diff case at step (3) "Already up to date" which is not an error, we could recover more elegantly:

  • Go back to dev branch
  • Delete temp-branch
  • Give a better message (not error, just say that there's nothing to do)

a few quick examples

Hi, this looks promising. It would be nice to have a few quick examples of how to use it. I presume I clone this repo and maybe copy the two files clean-push and 4-way-diff into my repo? Then call them via sh clean-push on the command line. I can, and will figure it out, but it would be nice if you just spoon fed it to me :)
Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.