GIT - HowTo's about "branches"

mail

How to undo a rebase ?

You may find yourself in one of these situations :

  1. incomplete rebase :
    1. you've run git rebase [options]
    2. Git replied with a "wall of text" having the word CONFLICT
    3. you're like 😱 and just want to return to before you run git rebase
    This is the easy situation since you can go back to that "before state" with : git rebase --abort
  2. complete rebase :
    1. you've run git rebase [options]
    2. you have / have not run into conflicts and have handled them more or less properly
    3. the git rebase command ended successfully and —as for Giteverything is going extremely well
    4. you're not satisfied with the code you get because of
      • wrong conflicts management decisions
      • other reasons
      and now, you want to go back to before you run git rebase. Read below
Whatever situation you ended in, and whatever "recovery solution" you applied, you've not fixed anything :
  • if you had a valid reason to perform a rebase in the first place, that reason still applies
  • aborting / resetting the operation just lets you return to a known + clean situation where you can handle things differently

Undoing a complete rebase :

  • What we want to do is return to a previous state of the Git repository, i.e. alter the index and the working directory, which is exactly what git reset --hard is for.
  • Once we know how to "jump back", we need to find out where to jump back to. git reflog will tell us.

example

  1. read my notes below about dry-running commands
  2. the current situation :
    31d1afd5 HEAD@{0}:  rebase finished: returning to refs/heads/feature
    31d1afd5 HEAD@{1}:  rebase: [commit message]							|
    												| A
    73595077 HEAD@{27}: rebase: [commit message]							|
    24066915 HEAD@{28}: rebase: checkout master
    70bef9e4 HEAD@{29}: checkout: moving from master to feature				  C
    24066915 HEAD@{30}: rebase finished: returning to refs/heads/master
    24066915 HEAD@{31}: pull upstream master: checkout 2406691540f36c1ca45fb3c7881ca9ca4b679465
    45e96175 HEAD@{32}: checkout: moving from feature to master
    70bef9e4 HEAD@{33}: rebase -i (finish): returning to refs/heads/feature			  C
    70bef9e4 HEAD@{34}: rebase -i (pick): [commit message]						|
    												| B
    
    with :
    • A : these are all the commits of the feature branch after I rebased them on master. I did 💩 with the conflicts and I want to get rid of this.
    • B : this is my previous work on the feature branch, before the rebase I want to undo
      It happens that this previous work includes a squash of commits (recognized via the rebase -i), but this has nothing to do with the rebase I want to undo.
    • C : candidates as for where to jump back to (the difference is the update of master from its remote)
  3. jumping back :
    HEAD is now at 70bef9e4 [commit message]
    And that's it !
  4. Some documentations suggest commands like :
    • git reset --hard HEAD@{33} : seems to do the job (but needs further tests to get it clear)
    • git reset --hard feature@{33} : moves back to some random point (???), I must have missed something

Dry-running commands

It is a safe practice to experiment commands / anything you're not completely sure of on a copy of your data instead of the actual data. To do so, I usually git clone my repository into a temporary place then play until I understand how things work and I feel confident to apply changes to the real data.
But since a cloned repository has an empty reflog, git clone is helpless here.

As a workaround, it is possible to run :

tmpDir="$(mktemp -d)" && cd "$tmpDir" && rsync -av "path/to/gitRepository/" .

mail

How to test / simulate / dry-run a merge without actually making changes ?

In this situation, there is no way to know What would happen if ... ? without actually trying it. However, it is pretty safe to try it :
  1. make sure there are no pending changes : git status should return nothing
  2. just try it : git merge myBranch
  3. 2 possibilities :
mail

How to list all files altered in a branch ?

	---o---o---o---o---o---o	main
	        \
	         o---o---o---o---o---o	myBranch
Listing stuff changed in a branch is the typical use case, but the method below actually works between any commits.
with :
which gives :
baseBranch='main'; featureBranch='myBranch'; git show $(git merge-base "$baseBranch" "$featureBranch").."$featureBranch" --name-only --format='' | sort -u
mail

Should I merge or rebase my feature branch to keep it up-to-date with master ?

Situation

We have :
A---B  master
 \
  C---D  feature
We want the feature branch to be up-to-date with master.
To do so, there are 2 options :

Comparison of options

command merge rebase
result
A---B  master
 \
  C-B-D-E  feature
A---B  master
     \
      C'-D'  feature
pros
  • reliable solution
  • resolve conflicts only once
  • fine for small teams and not very highly active master branch (few changes to sync from master to feature)
  • merging is a non-destructive operation : it only adds the merge commit and makes no change to the branches
  • linear history / tramlines : easier to follow
  • feature stays parallel to master
  • feature only contain feature-related changes
  • no merge commits
cons
  • "mixes" master and feature commits into feature, making it more difficult to track changes (git log, git bisect, ...)
  • the above is made even more complex if feature lasts for a long time
  • on a large repo with many committers / branches, this will create many redundant merge commits (and wide tramlines in the commit history)
  • MUST NOT be used on public branches
  • _can_ lead to fixing conflicts repeatedly when rebasing
  • updating feature with rebase throughout its life makes it a "private" branch, i.e. :
    • code that is not shared to others
    • code living on your hard disk only (SPOF)
  • impairs history :
    • loses chronological order of creation of code (but no big deal, actually)
    • loses context provided by a merge commit : you can't see when upstream changes were incorporated into feature

Which is the best ? Which should I use ?

  1. if applicable (private branch), try rebase
  2. if it turns into conflict resolution hell, give up and use merge
  3. move on
But don't forget :
  • the answer to the merge vs rebase question is mostly context-dependent
  • you may avoid most of the pain by having smaller features, meaning shorter-lived feature branches
  • don't forget to interactively rebase feature (private commits only, as usual) from time to time to :
    • re-order commit
    • squash commits
so that the development history makes more sense.
mail

How to stop tracking a remote branch ?

mail

Workflow of upstream + fork + feature branch ending with a merge --squash

This procedure looks overkill.
  • Maybe I was trying to achieve something very specific when writing this (but forgot to note how this was special ).
  • Or maybe I've gained some extra experience with Git since that time, and would do that differently today.
Anyway, I can't see what using this temporary "squash branch" brings.
Remember the use case that made all this necessary.

Preliminary

Have a look to the definitions if you're not familiar with these terms :

The workflow :

  1. setup :
    • I have an upstream repository : upstream
    • upstream is forked into myFork
    • my local repository is a clone of myFork
  2. initial status of my local repository :
    ‌
    	---o---o---o master
    
    
  3. I create a feature branch and work there :
    1. git checkout -b feature
    2. code then git add
    3. git commit
    4. repeat
    ‌
    	---o---o---o---o---o---o master
    	            \
    	             o---o---o feature
    
    
  4. the work on my feature is done, time to share it with others. To do so, I update master of local repo + fork :
    1. git checkout master
    2. git pull upstream master
    3. git push myFork master
    The point of this step is to receive the changes made by others on master, so that conflicts —should there be any— arise on my local repository rather than later while merging on upstream.
  5. I create a branch —from master— to squash my feature branch into :
    1. git checkout master
    2. git checkout -b feature_squash
    ‌
    	---o---o---o---o---o---o master feature_squash
    	            \
    	             o---o---o feature
    
    
  6. I merge squash my feature branch into the squash branch :
    1. git merge --squash feature
      Squash commit -- not updating HEAD
      Automatic merge went well; stopped before committing as requested
    2. git commit
    3. summarize the list of commit messages into something explaining the single resulting commit :
      did this and that as requested by ticket #xyz
    ‌
    	---o---o---o---o--o---o master
    	            \          \
    	             \          o feature_squash
    	              \        /
    	               o--o---o feature
    
    
  7. I merge my squash branch into master :
    1. git checkout master
    2. git merge feature_squash
    ‌
    	---o---o---o---o--o---o---o master feature_squash
    	            \          \ /
    	             \          o
    	              \        /
    	               o--o---o feature
    
    
  8. Delete the squash branch :
    git branch -d feature_squash
    ‌
    	---o---o---o---o--o---o---o master
    	            \          \ /
    	             \          o
    	              \        /
    	               o--o---o feature
    
    
  9. Push the changes (now on master) to my fork :
    git push myFork master
  10. merge myFork into upstream : to be done with GitLab
  11. delete the local feature branch :
    Since feature has been merged into feature_squash —that doesn't exist anymore— Git will consider feature as not merged and you'll have to force it with :
mail

How to forbid commits on the master branch ?

Situation

After hours of sweating / swearing / cleaning, you decide it would be wise to set up some safeguard to avoid living this again.

The title of this article as well as the solution below refer to master because it's a pretty common situation, but you can —of course— apply this to any branch .

Details

Here come the Git hook scripts :
Client-side hooks are not copied when cloning a repository (details).

Solution

  1. cd root/of/myGitRepo
  2. cat << 'EOF' > .git/hooks/pre-commit
    #!/usr/bin/env bash
    
    currentBranch="$(git rev-parse --abbrev-ref HEAD)"
    if [ "$currentBranch" == 'master' ]; then
    	echo 'None shall pass!'
    	exit 1
    fi
    EOF
    chmod +x .git/hooks/pre-commit
    
  3. Check :
    ls -l .git/hooks/pre-commit && cat $_

Alternate solution

I've found ONE edge case where I still need to be able to commit to master : when finalizing a merge --squash. So here's a hack to workaround the pre-commit hook script without entirely disabling it :
  1. add to the code :
    #!/usr/bin/env bash
    
    currentBranch="$(git rev-parse --abbrev-ref HEAD)"
    if [ "$currentBranch" == 'master' -a "$c2m" != 'yes' ]; then
    	echo 'None shall pass!'
    	exit 1
    fi
  2. explicitly raise a flag to actually commit to master :
    c2m=yes git commit
mail

How to merge a branch as a single commit (i.e. merge + squash) ?

Situation

I have :
	          F-------G myBranch
	         /
	A---B---C---D---E master
A standard merge would give :
	          F-------G myBranch
	         /         \
	A---B---C---D---E---H master
I want :
	A---B---C---D---E---H master myBranch

Solution

mail

How to track a remote branch ?

When starting working on an existing remote branch :

git checkout --track remote/branch
Branch branch set up to track remote branch branch from remote.
Switched to a new branch 'branch'
This :
  1. creates the local branch branch
  2. sets remote/branch as the upstream of the new local branch branch
  3. checkouts the new local branch branch
You can not run :
git checkout --track remote/branch myLocalBranch
fatal: Cannot update paths and switch to branch 'branch' at the same time.
Did you intend to checkout 'myLocalBranch' which can not be resolved as commit?

When publishing a local branch :

git push -u remote branch

Anytime :

git branch -u remote/branch localBranch
Branch localBranch set up to track remote branch branch from remote.
localBranch must exist before doing this.
git branch -u remote/branch
Branch currentLocalBranch set up to track remote branch branch from remote.
This construct declares remote/branch as the upstream of the currently checked out local branch currentLocalBranch, which may not be your intention.
mail

How to list commits from a specific branch only ?

These commands also state at which commit 2 branches diverged : the oldest reported commit is the common ancestor of both branches.
mail

How to move commits to another branch ?

This typically happens when you've committed on master, then realized that those commits better suit a feature branch. In other words, how may I go from this :

	A---B---C---D---E master

to this :
	      C---D---E newBranch
	     /
	A---B master

Before going further, make sure you have no uncommitted changes left.

Method 1 : (source) :

  1. create a new branch + "jump" on it :
    git checkout -b newBranch
  2. make master point to the commit that is the base of newBranch :
    git branch -f master HEAD~3
    3 is the number of commits to move from master to newBranch

Method 2 (source) :

git branch newBranch      # Create a new branch, saving the desired commits
git reset --hard HEAD~3   # Move master back by 3 commits (GONE from master)
git checkout newBranch    # Go to the new branch that still has the desired commits


NB : instead of resetting to a nb of commits, you can reset until a specific commit ID :
git reset --hard a1b2c3d4

How it works (source) :

You want to go back to C, and move D and E to the new branch. Here's what it looks like at first:

A-B-C-D-E (HEAD)
        ↑
      master

After git branch newBranch:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

After git reset --hard HEAD~2:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

Since a branch is just a pointer, master pointed to the last commit. When you made newBranch, you simply made a new pointer to the last commit. Then using git reset you moved the master pointer back two commits. But since you didn't move newBranch, it still points to the commit it originally did.

Method 3 : cherry-picking (source) :

NB : if you checkout newBranch from the existing master branch it ALREADY has those three commits included in it, so there's no use in picking them. At the end of the day to get what the OP wanted, you'll still have to do some form of reset --hard HEAD.

https://stackoverflow.com/questions/1628563/move-the-most-recent-commits-to-a-new-branch-with-git#comment-36690393



Step 1 - Note which commits from master you want on a new branch :

git checkout master
git log

Note the hashes of (say 3) commits you want on newBranch. Here I shall use:
C commit: 9aa1233
D commit: 453ac3d
E commit: 612ecb3

    Note: You can use the first seven characters or the whole commit hash

Step 2 - Put them on the new branch

git checkout newBranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

NB : the order is important. You want to do the oldest commits first