gitnotes
Markdown

gitnotes

Note Git keeps its state by using a series of files and directories.

git log has many options to make viewing of history a pleasant experience.

man git-log and search for --graph and --decorate . Quickly find and read the description of each option git log --graph --decorate

If you don't know what a branch is, that is ok, we will get there. Unfortunately we only have one commit, therefore our --graph option is less impressive. Don't worry we will make extensive use. What does show up is the * showing the graph has only one node in the output

  1. Git internal representations To understand git you must go past the porceline and check out the plumbing SHAs git commits come with a sha (a hash with 0-9a-f characters). You can specify the first 7 characters of a sha for git to identify what you are referring to. To get the sha of a commit, we can use the log command and copy the long hex string

  2. Problem Find the commit sha of your first commit (copy it to your system clipboard)

  3. Solution git log You should see something like this commit 5ba786fcc93e8092831c01e71444b9baa2228a4f (HEAD -> master) Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 21 19:40:56 2024 -0700 batman

commit 5ba786fcc93e8092831c01e71444b9baa2228a4f ---------^ this is the sha Question Why is your sha different than mine?

Answer sha: SHA stands for Secure Hashing Algorithm. SHA is a modified version of jMD5 Second, your sha, if you remember me saying, is the combination of changes you have made, author, time, etc etc

  1. Problem This next part will be a bit hard, but its doable. Can you find your commit's file(s) of data within .git folder. See if you can cat out any data

hint: Your commit's sha is a key piece of information hint: The first 2 characters of the commit sha...

  1. Solution

Upon initial look via ls, you will see a nothing that looks familiar. ➜ my-first-git git:(master) ls -la .git total 52 drwxrwxr-x 8 ThePrimeagen ThePrimeagen 4096 Jan 21 12:10 . drwxrwxr-x 3 ThePrimeagen ThePrimeagen 4096 Jan 21 12:10 .. drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 10:29 branches -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 7 Jan 21 12:10 COMMIT_EDITMSG -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 92 Jan 21 10:29 config -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 73 Jan 21 10:29 description -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 23 Jan 21 10:29 HEAD drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 10:29 hooks -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 209 Jan 21 12:10 index drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 10:29 info drwxrwxr-x 3 ThePrimeagen ThePrimeagen 4096 Jan 21 10:52 logs drwxrwxr-x 10 ThePrimeagen ThePrimeagen 4096 Jan 21 12:10 objects drwxrwxr-x 4 ThePrimeagen ThePrimeagen 4096 Jan 21 10:29 refs But within the objects directory you should see at least one interesting entry ➜ my-first-git git:(master) ls -la .git/objects total 28 drwxrwxr-x 7 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 . drwxrwxr-x 8 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 .. drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 4e drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 5b drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 9a drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 info drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 pack Do you see anything that is familiar in here? I do. My commit, 5ba786fcc93e8092831c01e71444b9baa2228a4f starts with 5b and so does a directory here. Lets ls that directory

➜ my-first-git git:(master) ls -la .git/objects/5b total 12 drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 . drwxrwxr-x 7 ThePrimeagen ThePrimeagen 4096 Jan 21 19:40 .. -r--r--r-- 1 ThePrimeagen ThePrimeagen 125 Jan 21 19:40 a786fcc93e8092831c0 You may now notice again that a786... is the remaining part of my commit sha, and yours exists in the same format!

  1. Observation Commits exist in the .git/objects directory with the first 2 letters as a directory, and the remaining 38 as a file. What's in its Pocketses, Precious? If we try to cat out the commit file we see nothing useful

➜ cat .git/objects/5b/a786fcc93e8092831c01e71444b9baa2228a4f x[ )@͢ <41M]V%qP9C'*"iܖ UfmA"DqFx3-C(U˅-YIw]0y6y1 @u ڟ`V?9r% Remember ALL of git state is stored in files. everything.

  1. The Tools of the Plumber There are ways to inspect files within the git's data store.
git cat-file -p <some-sha>

This will echo out the contents of the sha. This can be a commit, a tree, or a blob (more on those in a bit)

  1. Problem

Can you get git cat-file -p to echo out the contents of first.md by inspecting the commit sha? You may have to have a few rounds of catting

Solution First start by git cat-file -p . You should see something similar. ➜ git cat-file -p 5ba786fcc93e8092831c01e71444b9baa2228a4f tree 4e507fdc6d9044ccd8a4a3061324c9f711c4667d author ThePrimeagen the.primeagen@gmail.com 1705891256 -0700 committer ThePrimeagen the.primeagen@gmail.com 1705891256 -0700 batman You will notice there is no first.md or its contents therefore we must be able to find something... Oh look at that, tree has a sha, lets try that ➜ git cat-file -p 4e507fdc6d9044ccd8a4a3061324c9f711c4667d 100644 blob 9a71f81a4b4754b686fd37cbb3c72d0250d344aa first.md Wait... is that first.md... ➜ git cat-file -p 9a71f81a4b4754b686fd37cbb3c72d0250d344aa

hello world Well, well, well, look at what the VSC has drug in... if it isn't our file! Blob 9a71f81a4b4754b686fd37cbb3c72d0250d344aa is literally first.md at the point of our first commit. Key Concepts • tree: tree is analagous to directory • blob: blob is analagous to file You probably noticed that the tree (directory), when cat'd, contains a single entry, a blob, which was named first.md. BIG TAKEAWAY

Git does not store diffs, git stores complete version of the entire source at the point of each commit. In other words, each commit contains all the information to completely reconstruct the source code that was tracked.

A Second Change

Problem

With your amazing git skillz, create a second file, second.md , insert some text, stage, and commit the file.

Solution vim second.md # Great editor to add such a wonderful change git add second.md git commit -m "second" [master 48d06ff] second 1 file changed, 2 insertions(+) create mode 100644 second.md Problem Explore your new git commit. What can you say about first.md ? What about second.md ?

Solution Use git log to get commit sha of your previous commit or use the line from your commit message [master 48d06ff] second . Now lets list out the contents of that commit ➜ my-first-git git:(master) git cat-file -p 48d06ff tree 6282551fedc655bc5ee9180ad67021c22245fdae parent 5ba786fcc93e8092831c01e71444b9baa2228a4f author ThePrimeagen the.primeagen@gmail.com 1706387467 -0700 committer ThePrimeagen the.primeagen@gmail.com 1706387467 -0700 second

Parents

All but the first commit will have a parent. Notice that our new commit has a parent!

Inspect the tree with git cat-file -p 6282551fedc655bc5ee9180ad67021c22245fdae ➜ my-first-git git:(master) git cat-file -p 6282551fedc655bc5ee9180ad67021c 100644 blob 9a71f81a4b4754b686fd37cbb3c72d0250d344aa first.md 100644 blob 7f112b196b963ff72675febdbb97da5204f9497e second.md Now compare the original tree from our first commit ➜ my-first-git git:(master) git cat-file -p 4e507fdc6d9044ccd8a4a3061324c9f 100644 blob 9a71f81a4b4754b686fd37cbb3c72d0250d344aa first.md Notice that our commit, 48d06ff , has the same first.md file, but with a newly added second.md ! So to manually reconstruct the entire file tree thus far, we just need to cat-file both first.md and second.md from the current commhut! That also means that git stores pointers to the ENTIRE worktree, not the entire worktree itself which means its significantly more efficient space wise. hopefully this makes git feel less magical. Just remember at some point every program is just a bunch of if statements, for loops, and variables. This is true even about git

← Previous Next → For Them Create an inner directory and do it again and show them the inner trees!

git config Earlier we went over the basics of git config by setting your user name and email. git config --add --global user.name "ThePrimeagen" git config --add --global user.email "the.primeagen@aol.com" --add In the previous section we discussed adding and added user.name and user.email. Remember every key is made up of two distinct parts, section and keyname

git config --add

. Problem Create and add 3 values with the section fem (frontend masters) but different keynames and we will use this for our platform for exploration. I'll use: fem.dev is great fem.marc is ok fem.git would

Solution ➜ my-first-git git:(master) git config --add fem.dev "is great" ➜ my-first-git git:(master) git config --add fem.marc "is ok" ➜ my-first-git git:(master) git config --add fem.git "would" Listing out values There are a couple ways you can list out config values.

• --list : where it will list out the entirety of the config. • --get-regexp : this takes a pattern and looks for all names matching Problem Can you list out all of fem value's with a single command?

Solution ➜ my-first-git git:(master) git config --get-regexp fem fem.dev is great fem.marc is ok fem.git would (END) To tell you the truth, I personally end up using git config --list | grep more often though it is not equivalent, it Problem Using what you know currently, can you change one of your values of your config that you have added. Take fem.dev as an example. Can you change that value to is amazing! instead? Verify that you have changed the value with --get-regexp

Solution We have only talked about --add and --list . by using --add we would execute the following: ➜ my-first-git git:(master) git config --add fem.dev "is amazing" But when we list out the values we see ➜ my-first-git git:(master) git config --get-regexp fem fem.dev is great fem.marc is ok fem.git would fem.dev is amazing

Problem What has happened here? This may come as a bit of a surprise but config keys are not unique. You can have the same key more than once. instead of using --list lets use --get and see what comes out Solution

➜ my-first-git git:(master) git config --get fem.dev is amazing Notice that it got the latest value. This seems pretty reasonable default. You can get all of the values of a key by using --get-all Unsetting You can "unset" a value and you can remove an entire section. Lets do both. --unset : Unsets one key --unset-all : Unsets all matching keys

Problem try to remove our duplicate key with --unset . If you don't know how to do this, check out the man page, man git-config and search for unset Solution ➜ my-first-git git:(master) git config --unset fem.dev warning: fem.dev has multiple values Notice that we get a warning. Does this mean we have removed one of the key value pairs? Or all of them? Well lets list out what we have ➜ my-first-git git:(master) git config --get-all fem.dev

is great is amazing (END) That means we did not delete a single key! You cannot unset when there are multiple keys. Problem Repeat above but use --unset-all

Solution ➜ my-first-git git:(master) git config --unset-all fem.dev ➜ my-first-git git:(master) git config --get fem.dev Fun fact configuration is just a file. that means your local configurations are probably in the .git folder

Problem find the git config in the .git folder and cat it out Solution ➜ my-first-git-repo git:(master) cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false

logallrefupdates = true [fem] marc = is ok git = would Sometimes when the magic is gone its almost disappointing... Problem Yeah! We were able to unset all of fem.dev's values! We can unset all fem.* values by using --remove-section . Remember a key is actually two parts. Use --get-regexp to validate you have removed everything

. . So remove the entire fem namespace If you don't know how to use --remove-section , jump into the main page and use search to find the section

Solution ➜ my-first-git git:(master) git config --remove-section fem ➜ my-first-git git:(master) git config --get-regexp fem (END) We have removed all the fem keys! Lets go!

Locations We have briefly touched on location. There are several locations for git configs and they merge together to produce your final config. The locations are: --system, --global, --local, --worktree and you can provide a file path for the config via --file When we added our user.name and email, we did it for all of our projects through --global, whereas our fem additions were --local since we didn't specify where to add them to. You can use --local and --global when using --get , --list , --add , and --unset . Problem

Try adding the same key fem.dev to --local and --global locations (one at a time) and see what happens when you execute --list and --get-regexp add to --global first Solution

➜ my-first-git git:(master) git config --global --add fem.dev ➜ my-first-git git:(master) git config --local --add fem.dev ➜ my-first-git git:(master) git config --get-regexp fem fem.dev is amazing fem.dev is amazing2 (END) ➜ my-first-git git:(master) git config --list --local core.repositoryformatversion=0 core.filemode=true core.bare=false core.logallrefupdates=true fem.dev=is amazing2 (END) Problem But what about --get ? Does that choose the "latest" or does it choose differently? Can you suss out the ordering mechanism for what value is retrieved via --get ?

Solution ➜ my-first-git git:(master) git config --get fem.dev is amazing2 That grabs the local version, but I also added it second. Lets add another Global property and see if we still grab local first ➜ my-first-git git:(master) git config --global --add fem.dev

➜ my-first-git git:(master) git config --get fem.dev is amazing2 So that means the following:

  1. grabs from more specific to less specific
  2. gets the latest value from the most specific category To prove #2 try executing git config --get --global fem.dev . We should get is greatest

Like all of programming Git config probably at one point seemed a bit weird and you executed one command that long time ago and you haven't done anything since. As you can see, config is really simple now that we have taken the time to learn it. As always I encourage you to review the git manual Problem Lets make our default branch from master to trunk . The config setting is init.defaultBranch Question should this be a local or global change?

Solution git config --global init.defaultBranch trunk This will change the default setting for all projects going forward. This does not mean that our current git projects have changed. Current projects will have to be rename.

Lets try it out! Lets try creating a new git repository. You will want to do this part as we will use this project for the rest of the course. cd to/where/i/create/projects/at mkdir hello-git cd hello-git git init Whatever name you have chosen should show up now as the default branch! ➜ hello-git git init Initialized empty Git repository in /home/ThePrimeagen/personal/hello-git/.➜ hello-git git:(trunk)

EVERY←TH PIrNeGvio YuOs U'LL NEED TO KNOW ABOUT GIT INTRO NCeOxNt F→IGURING GIT Configuring Git – Everything You'll Need to Know About Git https://theprimeagen.github.io/fem-git/lessons/intro/config

Git Branching You don't always want to be developing on the main branch. Sometimes you need a feature that is developed off the main line, such that you can return to the main line, update the code, branch off, and perform some immediate needed fix. This is where git branches come in. They are cheap, virtually free, to create. With our of understanding git internals this will become much more clear throughout this section Problem

Since we are in a new git repo, lets create our initial commit. We will be using the hello-git you just created previously to test the trunk default branch setting I would like a README.md with one line, A , and a commit message of A Solution ➜ hello-git git:(trunk) echo "A" > README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A' [trunk (root-commit) cb75afe] A 1 file changed, 1 insertion(+) create mode 100644 README.md

Creating branches Creating branches is easy git branch foo Will create a new branch named foo

Problem Try it out, create a branch named foo Solution ➜ hello-git git:(trunk) git branch foo ➜ hello-git git:(trunk)

Well... what happened? • A branch was created pointing to the same commit as trunk • We remain on trunk List out branches We can observe this by executing git branch . git branch will display all of our local branches git branch

Problem List out your branches. How do you know what branch you are on? Use git log with --decorate to see where the branches point at and prove that foo points to the same commit as trunk Side Note If you use git log and you see the same output as when you use --decorate this happens. There are some internal settings that automagically make this happen git log > out cat out you may notice that you lose the branching information that you had.

Solution git branch foo

trunk

(END) That shows we have 2 branches, one foo and one trunk . trunk is selected, and you can tell because of the * lets execute git log and write down our sha for this specific commit commit cb75afebfac407bfc860dd854b626322a6dc8345 (HEAD -> trunk, foo) Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:04:01 2024 -0700 You will notice that the commit has HEAD -> trunk, foo . foo is pointing to this commit. A branch POINTS to a commit, it is not a set of changes.

A Touch of Git Foo Can you find your branch details in .git ? Ahh yes... its right there... ➜ hello-git git:(trunk) cat .git/refs/heads/foo cb75afebfac407bfc860dd854b626322a6dc8345 ➜ hello-git git:(trunk) cat .git/refs/heads/trunk cb75afebfac407bfc860dd854b626322a6dc8345

Switching branches You can switch branches easily in two ways git switch git checkout checkout is a more versatile operation and i am personally just in the habbit of using it. You can use whatever you want / are comfortable with Problem

switch to branch foo Solution ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo'

Problem Now that you are foo lets create another commit with message B and a new line added to second.md of "B" Then do it again, except replace B with C When finished use git log with what options you want, to see if you can see some differences now! This should be the setup you should have at the end of this B - C foo / A trunk

Solution ➜ hello-git git:(foo) echo "B" > second.md ➜ hello-git git:(foo) git add . ➜ hello-git git:(foo) git commit -m 'B' [foo 4ad6ccf] B 1 file changed, 1 insertion(+) create mode 100644 second.md ➜ hello-git git:(foo) echo "C" > second.md ➜ hello-git git:(foo) git add . ➜ hello-git git:(foo) git commit -m 'C' [foo 16984cb] C 1 file changed, 1 insertion(+), 1 deletion(-) Now lets execute git log and see if we can see a difference commit 16984cb1725b0bfeff43f60f92b3467ee6d525e3 (HEAD -> foo) Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:17:11 2024 -0700 C commit 4ad6ccf530547ce89cfd90cd7a6e747f9531d917 Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:15:38 2024 -0700 B commit cb75afebfac407bfc860dd854b626322a6dc8345 (trunk) Author: ThePrimeagen the.primeagen@gmail.com

Date: Sun Jan 28 10:04:01 2024 -0700 A 2 things:

  1. Notice that HEAD -> foo , or current location of git points to foo, which is 16984cb
  2. trunk is still at A . Trunk has not been updated with this code Fun side-note • try out --graph if you didn't! • try out --oneline if you haven't • try all three options together! You can delete branches If you create a branch and you wish to delete it you can use -d and -D . For more information read up in the git manual, man git-branch

Commits locked in... Ok, now that we have a branch full of changes... what do we do with it?

← Previous Next →

Prereq: You have to have the previous section finished and have the same state B --- C foo / A trunk Problem Lets start off by creating some more commits and adding them to trunk . This way our branches diverge. Meaning they have commits that are unique in both and have a common ancestor, A .

Please add D and E in the same way we added B and C to trunk Desired State: B --- C foo / A --- D --- E trunk Solution ➜ hello-git git:(foo) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) echo "D" >> README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'D' [trunk 79c5076] D 1 file changed, 1 insertion(+)

➜ hello-git git:(trunk) echo "E" >> README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'E' [trunk a665b08] E 1 file changed, 1 insertion(+) Now we have the following setup B --- C foo / A --- D --- E trunk and our logs have commit messages that should make it easy to track. So now we can talk about git merge and rebase Git Merge and Rebase Remember A commit is a point in time with a specific state of the entire code base.

We have work done, but its on another branch. We need to get it back into our main branch, trunk ... but how? There really is only one strategy to merge code from one branch to another, but depending on the state different outcomes can happen. rebase can help put a branch into a pristine state What is a merge? A merge is attempting to combine two histories together that have diverged at some point in the past. There is a common commit point between the two, this is referred to as the best common ancestor ("merge base" is often the term used in the docs).

To put it simply, the first in common parent is the best common ancestor. git then merges the sets of commits onto the merge base and creates a new commit at the tip of the branch that is being merged on with all the changes combined into one commit. there is also extra information in the logs about these types of commits How to merge Merging is quite simple. The branch your on is the target branch and the branch you name in will be the source branch.

git merge Problem Given the following state, merge foo onto trunk . Since we want to keep foo and trunk in its current state for other problems later in this lesson. Please start by branching off of trunk . I named mine trunk-merge-foo Finally, when you are done use git log to see the resulting state of trunk-merge-foo State of your git B --- C foo

/ A --- D --- E trunk-merge-foo Solution ➜ hello-git git:(trunk) git checkout -b trunk-merge-foo Switched to a new branch 'trunk-merge-foo' git checkout -b trunk-merge-foo is the same thing as the following: git branch trunk-merge-foo git checkout trunk-merge-foo

Fun fact From man git-switch you can see the following: git switch [] (-c|-C) [] Meaning we could of created a new branch by using git switch -c trunk-merge-foo and switched to it. lets merge in foo into trunk-merge-foo ➜ hello-git git:(trunk-merge-foo) git merge foo At this point you are presented with the EDITOR . Git will use your system's default editor for you to accept/make any changes to commit messages. You are presented with the message that will be in the logs after merging these two branches together. I typically do not edit this merge message Merge branch 'foo' into trunk-merge-foo

  1. Please enter a commit message to explain why this merge is necessary,
  2. especially if it merges an updated upstream into a topic branch.

  1. Lines starting with '#' will be ignored, and an empty message aborts
  2. the commit. If vim is your default git editor :wq to save the message and confirm the commit ➜ hello-git git:(trunk-merge-foo) git merge foo Merge made by the 'ort' strategy. second.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 second.md Lets use log to check out what has happened here • --parents adds 1 to two extra shas signifying the parent chain. This is duplicated by --graph but instead of a graphical representation, its with shas. ➜ hello-git git:(trunk-merge-foo) git log --oneline --decorate --graph * ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch |
    | * 16984cb 4ad6ccf (foo) C | * 4ad6ccf cb75afe B

| a665b08 79c5076 (trunk) E

| 79c5076 cb75afe D

|/

cb75afe A

How to read the lines | * 16984cb 4ad6ccf (foo) C commit 16984cb has a parent of 4ad6ccf with a named branch of foo with a commit message of C The only commit that has a different look is the first line of the logs, which is the latest commit.

ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch

ccf9a73 has two parents, a665b08 and 16984cb . If you look at those commits, they are the commits of trunk and foo . git merge merged those two commits together by finding the best common ancestor (merge

base cb75afe ) and playing the commits one at a time, start at cb75afe , creating a new commit, a merge commit, ccf9a73 . Problem Create the following git setup: X - Y bar / A --- D --- E trunk That means: • branch bar off trunk • add two commits with message X then Y • create your changes, X and Y , in bar.md

Solution First checkout trunk then use trunk to create branch bar ➜ hello-git git:(trunk-merge-foo) git checkout trunk ➜ hello-git git:(trunk) git checkout -b bar Create the two commits, X and Y , on bar ➜ hello-git git:(bar) echo "X" > bar.md ➜ hello-git git:(bar) git add bar.md ➜ hello-git git:(bar) git commit -m "X" [bar 2f43452] X 1 file changed, 1 insertion(+) create mode 100644 bar.md ➜ hello-git git:(bar) echo "Y" >> bar.md ➜ hello-git git:(bar) git add bar.md

➜ hello-git git:(bar) git commit -m "Y" [bar b23e632] Y 1 file changed, 1 insertion(+) we can verify that we have similar histories by using git log again. ➜ hello-git git:(bar) git log --oneline --decorate --graph

  1. You should see the same ordering

b23e632 (HEAD -> bar) Y

2f43452 X

a665b08 (trunk) E

79c5076 D

cb75afe A

Problem

merge bar onto trunk . Do not create a separate branch this time. Once you merge see if you can spot the difference between a "3 way merge" vs a "fast forward" merge. Solution First we simply checkout trunk then execute git merge bar ➜ hello-git git:(bar) git checkout trunk M README.md Switched to branch 'trunk' ➜ hello-git git:(trunk) git merge bar Updating a665b08..b23e632 Fast-forward bar.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 bar.md

First difference In the merge notes we see the following line: Fast-forward That means we have a fast forward merge Second, the logs You can view this FF merge with git log ➜ hello-git git:(trunk) git log --oneline --decorate --graph

b23e632 (HEAD -> trunk, bar) Y

2f43452 X

a665b08 E

79c5076 D

cb75afe A

Excalidraw what happens

Rebase Rebasing often gets a bad wrap. Part of this is because people don't really know why or when to use rebase and will end up using it incorrectly and thus yelling on the twitters that it "ruins their entire life." Gotta love twitter. With the following repo setup B --- C foo / A --- D --- E --- X --- Y trunk We can demonstrate the power of rebase. What rebase will do is update foo to point to Y instead of A .

B --- C foo / A --- D --- E --- X --- Y trunk THAT IS IT That is all rebase does here. It updates the commit where the branch originally points to This also means that in we decide to merge foo into trunk we can do a ffmerge! What Rebase Does

The basic steps of rebase is the following:

  1. execute git rebase . I will refer to the current branch as later on
  2. checkout the latest commit on
  3. play one commit at a time from
  4. once finished will update to the current commit sha How rebase works is important, it will lead to complication down the road if you don't understand this key fact We will address that later Problem

rebase foo with trunk . Please create a separate branch foo-rebasetrunk . This branch should be created off of foo . Once you have rebased foo-rebase-trunk with trunk check out the git logs with --graph and --decorate to see what has happened ( --oneline can make it easier to read) Solution First we will create a new branch off of foo and call it foo-rebase-trunk ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo' ➜ hello-git git:(foo) git checkout -b foo-rebase-trunk Switched to a new branch 'foo-rebase-trunk' Now we need to perform the rebase

➜ hello-git git:(foo-rebase-trunk) git rebase trunk Successfully rebased and updated refs/heads/foo-rebase-trunk. git log time to view what has happenend ➜ hello-git git:(foo-rebase-trunk) git log --oneline --decorate --graph

  1. you should have same history

3ade655 (HEAD -> foo-rebase-trunk) C

d810248 B

b23e632 (trunk, bar) Y

2f43452 X

a665b08 E

79c5076 D

cb75afe A

You will see that trunk (which contains bar after our fast forward merge) is now the new base for foo-rebase-trunk . In other words, foo is no longer diverging from trunk . If we choose to merge foo into trunk we can do so via ff-merge (no merge commit).

Some pros When using rebase you can have a clean history with no merge commits. If you are someone who uses git log a lot this can really help with searching. Some cons It alters history of a branch. That means that if you already had foo on a remote git, you would have to force push it to the remote again. We will go over this more it shortly Some cautionary words NEVER CHANGE HISTORY OF A PUBLIC BRANCH. In other words, don't ever change history of trunk . But your own personal branch? I don't think it matters and i think having a nice clean history can be very beneficial if you use git to search for changes through time.

Content Licensed Under CC-BY-NC-4.0 Code Samples and Excercises Licensed Under Apache 2.0 ← Previous Next → Wait.. isn't there more? Yes, there is a lot more, but we are not going to go over it without more foundational knowledge

Site Designed by Alex Danielson

HEAD You have seen it a bunch of times and probably can guess what it means... Throughout this course you may have notice that whenever we call git log there a HEAD within the logs. What is HEAD ? Lets do some basic experimenting first ➜ hello-git git:(foo-rebase-trunk) git checkout trunk

Switched to branch 'trunk' ➜ hello-git git:(trunk) git log --oneline --decorate --graph

b23e632 (HEAD -> trunk, bar) Y

... ^--- we currently have trunk checked out swapping branches and doing another git log will result in similar results. ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo' ➜ hello-git git:(trunk) git log --oneline --decorate --graph

16984cb (HEAD -> foo) C

... ^--- we currently have foo checked out Problem In the .git folder can you tell what HEAD is pointing to?

Solution ➜ hello-git git:(foo) cat .git/HEAD ref: refs/heads/foo That means as foo updates HEAD does not have to! Reflog

Its not just a wizards tool Introducing git reflog . The default command of git reflog allows you to see where head has been. Thought Exercise Give reflog command a try. What do you see. Think about what you have had checked out.

Answers ➜ hello-git git:(trunk) git reflog

  1. You should see something similar if you have been following along 16984cb (HEAD -> foo) HEAD@{0}: checkout: moving from trunk to foo b23e632 (trunk, bar) HEAD@{1}: checkout: moving from foo-rebase-trunk to trunk 3ade655 (foo-rebase-trunk) HEAD@{2}: rebase (finish): returning to refs/heads/foo-3ade655 (foo-rebase-trunk) HEAD@{3}: rebase (pick): C d810248 HEAD@{4}: rebase (pick): B ... If you look carefully you will see every checkout you have done of any branch is listed, in time based ordering Search Can you find where this information is stored inside .git

Solution In reflog command ➜ hello-git git:(foo) git reflog -3 44016b0 (HEAD -> foo) HEAD@{0}: checkout: moving from foo-rebase-trunk to foo 11a3f97 (foo-rebase-trunk) HEAD@{1}: rebase (finish): returning to refs/heads/foo-11a3f97 (foo-rebase-trunk) HEAD@{2}: rebase (pick): C In .git/logs/HEAD ➜ hello-git git:(foo) cat .git/logs/HEAD | tail -3 6fd6e31690d138de9380544d01842841b0d37963 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 44016b016fd77883d98b1fce4c3dea2cf877f408 Again. Its not magic

Problem This will be an interesting set of tasks to do, but can be quite useful in understanding reflog.

  1. create a new branch off of trunk, call it baz .
  2. add one commit to baz . Do it in a new file baz.md
  3. switch back to trunk and delete baz ( git branch -D baz from earlier)
  4. can you bring back from the dead the commit sha of baz ?

Solution First thing we will do is create the baz branch then add a commit to it ➜ hello-git git:(foo) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) git checkout -b baz Switched to a new branch 'baz' ➜ hello-git git:(baz) echo "baz" > baz.md ➜ hello-git git:(baz) git add baz.md ➜ hello-git git:(baz) git commit -m 'Baz' [baz f330d23] Baz 1 file changed, 1 insertion(+) create mode 100644 baz.md Next, lets switch back to trunk and delete the branch baz ➜ hello-git git:(baz) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) git branch -D baz Deleted branch baz (was f330d23). Now we recover the branch via reflog (should be obvious because i just talked about it) ➜ hello-git git:(baz) git reflog

b23e632 (HEAD -> trunk, bar) HEAD@{0}: checkout: moving from baz to trunk f330d23 HEAD@{1}: commit: Baz # <--- there is our target commit b23e632 (HEAD -> trunk, bar) HEAD@{2}: checkout: moving from trunk to baz ... Now that we see f330d23 is our target commit, what can we do to recover the work? There are a lot of possibilities. Problem Use our knowledge of how git plumbing works to retrieve the contents of a commit, use this super power to grab out the file baz.md

Solution To get the information we need we will have to use the commit to get the tree sha and the tree sha for the blob, file baz.md , contents

  1. Step 1, get the file tree try cat-file'ing the commit sha ➜ hello-git git:(trunk) git cat-file -p f330d23 tree d2d8e10a88b4e985003930d45c5c488abe712e6b # <-- the tree parent b23e6320e6fba64d93338543dcbcdcc9caadb71e author ThePrimeagen ThePrimeagen@netflix.com 1707069560 -0700 committer ThePrimeagen ThePrimeagen@netflix.com 1707069560 -0700 Baz
  2. Step 2, print out the trees content. baz.md is our taget file ➜ hello-git git:(trunk) git cat-file -p d2d8e10 100644 blob d3045e2c2d21fcf774700b3d5fa681cf26b300ad README.md 100644 blob 16858db7afb62f3e027d8f9379085d3567bcac62 bar.md 100644 blob 76018072e09c5d31c8c6e3113b8aa0fe625195ca baz.md # <-- target file
  3. Step 3, print the file and copy the contents! ➜ hello-git git:(trunk) git cat-file -p 7601807 > baz.md Pure wizardry

Problem Lets not use the internals, what is another way, using the commands we have gone over thus far to get the same information. We flexed... but we didn't have to. State the change, don't perform it Solution Remember a branch is just a pointer to a commit ➜ hello-git git:(trunk) git merge f330d23

Updating b23e632..f330d23 Fast-forward baz.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 baz.md I can just merge that change directly into trunk! Warning If histories have diverged far enough, this could cause some problems as you wouldn't just be merging the one commit, but all the commits in betwixt f330d23 and trunk Cherry Pick from man git-cherry-pick Given one or more existing commits, apply the change each one introduces, recording a new commit for each. This requires your working tree to be clean (no modifications from the HEAD commit).

Problem Git cherry-pick a try and see if you can also get the changes from baz into trunk Solution

Content Licensed Under CC-BY-NC-4.0 Code Samples and Excercises Licensed Under Apache 2.0 Site Designed ← Previous by Alex Danielson Next → ➜ hello-git git:(trunk) git cherry-pick f330d23 [trunk e6d9d4b] Baz Date: Sun Feb 4 10:59:20 2024 -0700 1 file changed, 1 insertion(+) create mode 100644 baz.md I have used cherry pick a ton of times throughout my career as a software dev Draw excalidraw the downsides of merge vs cherry-pick

Remote Git Often we need code changes that have been created by our fellow frienemies. But how do we get their changes into our repo? Or how do we push our changes to someone else repo? It doesn't have to be remote... Often we think of remote repos as github or gitlab, but it doesn't have to be that way. If you have never used git, maybe remote is an odd term.

A remote is simply a copy of the repo somewhere else. Problem Create a new repo, remote-git . Lets initialize it as an empty git repo using git init

Solution ➜ hello-git git:(trunk) cd /path/to/project git init remote-git Initialized empty Git repository in /path/to/project/remote-git/.git/ ➜ project cd remote-git ➜ remote-git git:(trunk) Distributed Version Control • A remote is just another git repo that is of the same project and has changes we may need. To add a remote the syntax is: git remote add

Problem Add hello-git as a remote. You should name it origin Solution

➜ remote-git git:(trunk) git remote add origin ../hello-git To check you can execute git remote -v to list out your remotes and their locations ➜ remote-git git:(trunk) git remote -v origin ../hello-git (fetch) origin ../hello-git (push) Gitism There are two naming conventions i have seen: Remote is project repo

typically when you have a remote git repo its called origin . This is the source of truth repo. Remote is your fork of some other repo sometimes you have your remote repo (fork), which you will name origin and you have the project repo which is typically named upstream . NOTICE: Earlier we set our git default branch name to trunk notice that it remained even on this new project. Now we need to merge in the changes from hello-git into our new repo remote-git .

Fetch we can fetch all the git state from our remote repository by executing git fetch . This wont update the current branches checked out, just where the origin/* has them set to. Problem

Try fetching all the branches from hello-git by using git fetch Solution ➜ remote-git git:(trunk) git fetch remote: Enumerating objects: 28, done. remote: Counting objects: 100% (28/28), done. remote: Compressing objects: 100% (18/18), done. remote: Total 28 (delta 5), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (28/28), 2.15 KiB | 2.15 MiB/s, done From ../hello-git

[new branch] bar -> origin/bar

[new branch] foo -> origin/foo

[new branch] foo-rebase-trunk -> origin/foo-rebase-trunk

[new branch] trunk -> origin/trunk

[new branch] trunk-merge-foo -> origin/trunk-merge-foo

Now those branches should look familiar considering we just got done making them!

Problem Using git log can you verify that the remote's trunk has been correctly merged into our git state but the current trunk we have checked out is not up to date? Solution

First we can see our trunk has no commits ➜ remote-git git:(trunk) git log fatal: your current branch 'trunk' does not have any commits yet trunk remains in its current state origin/trunk has been updated We verify we have the remote commits in our git state ➜ remote-git git:(trunk) git log origin/trunk b23e632 (origin/trunk, origin/bar) Y 2f43452 X a665b08 E 79c5076 D cb75afe A Notice the branch names pointing to commit b23e632 are origin/trunk and origin/bar . Note IMPORTANT: Branch names can contain the remote they come from.

NOTICE if we wish to see what branch es came down with git fetch we can execute git branch -a (git branch all) to see all branches that currently exist. Anytime you see a branch that is / it means that it is the last known state of the repo's (branch). Practically what this means is that at any moment you are likely out of sync with your remote. That is ok. That is the wonders of git. You don't have to update, you can use yours until you are ready to fetch in changes. Now convenience is a real thing. I try to update regularly. The large the out of sync is, the likelihood of conflict goes north

Problem Lets update our trunk with origin/trunk . Then lets use git log to validate those commits have been merged Solution ➜ remote-git git:(trunk) git merge origin/trunk ➜ remote-git git:(trunk) Well... that wasn't very climatic was it? Lets see what we got! ➜ remote-git git:(trunk) git log --oneline b23e632 (HEAD -> trunk, origin/trunk, origin/bar) Y

2f43452 X a665b08 E 79c5076 D cb75afe A You will now see that our local trunk matches our origin/trunk Git pull Fetching is always a good idea. It keeps your local repo's remotes up to date, but it doesn't alter your state. The thing is that a lot of the time you just want the changes merged for you into your branch. This can be done with one convenient command, git pull

git pull Problem

  1. Add a line at the end of README.md in hello-git and commit it with message A remote change .
  2. Execute git pull in remote-git
  3. Think about the error. Why does it exist?

Solution ➜ remote-git git:(trunk) cd ../hello-git ➜ hello-git git:(trunk) echo "remote-change" >> README.md ➜ hello-git git:(trunk) git add README.md ➜ hello-git git:(trunk) git commit -m "A remote change" [trunk 42afc8d] A remote change 1 file changed, 1 insertion(+) Now lets swap back to remote-git and pull in the changes instead of fetch / merge ➜ remote-git git:(trunk) git pull remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 271 bytes | 271.00 KiB/s, done From ../hello-git b23e632..42afc8d trunk -> origin/trunk There is no tracking information for the current branch. Please specify which branch you want to merge with. See git-pull(1) for details. git pull If you wish to set tracking information for this branch you can

git branch --set-upstream-to=origin/ trunk You will notice we failed. Why? The reason is that we did not setup our trunk branch to track the origin/trunk branch. Git will not automatically track state in a "remote" because that may not be what you want to do. Therefore if you git pull it wont know where to pull from since nothing has been specified. This becomes even more obvious once you have more than one remote. Its often convenient to setup tracking because we can use push and pull without specifying the target branch. Git assumes that just because two branches have the same name doesn't mean they are the same. So you need to tell git to track branches manually on preexisting branches. Problem

Read the error again and figure out how to "track" the remote branch. Once you execute the command then git pull to the origin. Use the tracking command that does not involve git pull Solution ➜ remote-git git:(trunk) git branch --set-upstream-to=origin/trunk trunk Branch 'trunk' set up to track remote branch 'trunk' from 'origin' ➜ remote-git git:(trunk) git pull # Now git pull is successful! Updating b23e632..42afc8d Fast-forward README.md | 1 + 1 file changed, 1 insertion(+) Since the changes were fetch ed with our previous pull we didn't have to re-fetch, but instead we simply merged with a "fast forward" merge strategy.

Remember all of our merge and rebase talk? Well now it really applies here. We were able to fast forward merge because we made no changes to trunk. Think about Github Hopefully this gives you some insight into what is actually happening with github/gitlab/stash. Its just another repo that everyone has agreed to commit to

What about rebase? Typically whenever I pull in changes from the remote authority repo I will rebase the changes. I do not like adding in a bunch of merge commits. This is a personal preference, but I think its a superiour one :) • A long lived branch with a bunch of merge commits is much more difficult to revert. • If every change is a single commit, then the ability to revert is very trivial. • I prefer to test my changes against the current state of master not against the current state i have fetched There is a config for it There are two strategies for rebase ing changes.

  1. add --rebase flag to a pull. git pull --rebase
  2. edit your config to make this behavior the default behavior. (good thing we know about how the config works) ➜ remote-git git:(trunk) git config --add --global pull.rebase Please don't update that setting now though Now we rebase remote code every time. You don't have to set this if you don't like the idea of rebasing from authority Git !pull The opposite of pull?

Yes, git push If you wish take your changes and move the remote repo, you can do this by using git push . Much like pull, if you are not "tracking" then you cannot simply git push but instead you will have to specify the remote and branch name. Lets make some changes to bar and push them to hello-git Fun Facts • git push : allows you to push and have it received with a different name • git push : will delete a branch on the remote

Problem Create a single commit, "CHANGE FROM REMOTE", as a one line change to the end of README.md to branch bar . Then push the changes back to hello-git validate the change made it to hello-git 's bar branch Solution ➜ remote-git git:(trunk) git checkout bar Branch 'bar' set up to track remote branch 'bar' from 'origin' Switched to a new branch 'bar' ➜ remote-git git:(bar) echo "Change from remote" >> README.md ➜ remote-git git:(bar) git add README.md

➜ remote-git git:(bar) git commit -m 'CHANGE FROM REMOTE' [bar aab17e0] CHANGE FROM REMOTE 1 file changed, 1 insertion(+) NOTICE When we checked out bar we automatically started tracking. We didn't have to create the branch. This is because locally we do not have a bar branch, but origin does. Also origin is our ONLY remote. Therefore it makes sense to create a local branch and track the remote it came from. Now lets push our change ➜ remote-git git:(bar) git push Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 12 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 306 bytes | 306.00 KiB/s, done Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To ../hello-git b23e632..aab17e0 bar -> bar And lets see if we see this on hello-git ➜ remote-git git:(bar) cd ../hello-git ➜ hello-git git:(trunk) git log bar

← Previous Next → aab17e0 (bar) CHANGE FROM REMOTE b23e632 Y 2f43452 X a665b08 E 79c5076 D cb75afe A Well look at that! We are sharing changes now! The astute observer will notice that bar 's branch on hello-git was moved forward... why?

Small note I will often say the phrase upstream to refer to hello-git . This is because i am treating hello-git similar to github . A Problem We All Face You have made a change... but you need to pull in changes from a remote repo, what do you do? commit changes then rebase or merge? (i would pick rebase)

The problems of merge and #1 If you create a commit with changes that are half baked just to pull in origin changes you will have your (potentially) broken commit, a merge commit, then your fixing commit. If you need to revert these changes, it will get more difficult (more on reverting later). This is a good case for rebase Your changes with rebase, will get put on top of all the incoming changes which allows your partial commit easier to work with. This style also makes squashing easier But i would rather have my partial commit not committed yet This is where stash comes in.

Worktrees Often the superior approach and we will discuss them later Stash git stash will take every change tracked by git (change to index + change to work tree) and store that result, much like a commit, into the "stash." To quote from the man page Use git stash when you want to record the current state of the working directory and the index, but want to go back to a clean working directory. The command saves your local modifications away and reverts the working directory to match the HEAD commit.

Stash is a STACK of temporary changes Operations You can push your changes into the stack by using git stash Stashes, much like commits, can come with a message ( -m "" ) git stash -m "my lovely message here"

Stashes can be listed out: git stash list git stash show [--index ] To pop the latest stash: git stash pop To pop a stash at an index: git stash pop --index # works well with git stash list Remember man git-stash is your friend. if you forget how a command works, please review the manual first! Its the authority of how to use the tool

Problem use branch trunk

  1. create an upstream change. Commit a small change to hello-git echo "upstream change" >> upstream.md
  2. add a small change to remote-git don't commit echo "downstream change" >> README.md
  3. now that we have an active tracked change pull in the upstream change What error do you get?

Solution

  1. create the origin changes

  2. make sure you are on branch trunk cd /path/to/hello-git echo "upstream change" >> upstream.md git add upstream.md git commit -m "upstream changes" [trunk 6849c67] upstream changes 1 file changed, 1 insertion(+) create mode 100644 upstream.md

  3. create the downstream changes but do not commit

  4. make sure you are on branch trunk cd /path/to/remote-git echo "downstream change" >> README.md

  5. validate that the changes are tracked by the worktree ➜ remote-git git:(trunk) git status On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

  6. Try to pull in origin changes ➜ remote-git git:(trunk) git pull error: cannot pull with rebase: You have unstaged changes. error: please commit or stash them. Error error: cannot pull with rebase: You have unstaged changes. error: please commit or stash them.

Problem Now that we have produced the error and the answer is clear: use stash. Lets play around with stash a bit more. To become more familiar perform the following:

  1. stash your changes
  2. view your stash list
  3. pop your stashed chages
  4. stash your changes but with a custom message
  5. create more changes and stash those so we have 2 in the list
  6. pull in the upstream's changes

Solution This will be a long set of changes, but they are all pretty simple.

  1. stash your changes ➜ remote-git git:(trunk) git stash Saved working directory and index state WIP on trunk: 42afc8d A remote ➜ remote-git git:(trunk) git status On branch trunk nothing to commit, working tree clean NOTE If you have changes to non indexed files then they will not be added to the stash command. Careful not to lose them.
  2. view your stash list ➜ remote-git git:(trunk) git stash list stash@{0}: WIP on trunk: 42afc8d A remote change (END)

To view the current stashed change (the one in position 0) use show diff --git a/README.md b/README.md index 9f276a6..2ca5a19 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ A D E remote-change +downstream change 3. pop your stashed chages ➜ remote-git git:(trunk) git stash pop On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Dropped refs/stash@{0} (e318e20fb946c2a78700611129d9ae040b4cc80c) 4. stash your changes but with a custom message ➜ remote-git git:(trunk) git stash -m "my very nice change about..." Saved working directory and index state On trunk: my very nice ➜ remote-git git:(trunk) git stash list stash@{0}: On trunk: my very nice change about...

  1. create more changes and stash those so we have 2 in the list Note Having named stashes can be useful if you come back a week later to a project and forgot what you have stashed / where you have put your changes ➜ remote-git git:(trunk) echo "some other change" >> README.md ➜ remote-git git:(trunk) git stash -m "other changes" Saved working directory and index state On trunk: other changes ➜ remote-git git:(trunk) git stash list stash@{0}: On trunk: other changes stash@{1}: On trunk: my very nice change about...
  2. pull in the upstream's changes ➜ remote-git git:(trunk) git pull Updating 42afc8d..6849c67 Fast-forward upstream.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 upstream.md

Problem Now we need to get back our original changes. Please pop the changes of our first stash. Remember the stash is a stack like data structure. Therefore your first change isn't the first item in the stash. Solution We need to use pop and --index to accomplish this.

➜ remote-git git:(trunk) git stash pop --index 1 On branch trunk Your branch is up to date with 'origin/trunk'. Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Dropped refs/stash@{1} (c58f8ce5300e4207031438236c5732901666a43a) ➜ remote-git git:(trunk) git commit -m 'greatest changes' [trunk 7282922] greatest changes 1 file changed, 1 insertion(+) Stashing is quite powerful and allows you to be able to bring in upstream changes without losing your work or creating commits which can be annoying to deal with! Worktrees

← Previous Next → We will talk more about these, but generally this is my favorite way to work in a fast EVERYTHI NevGol vYinOg Uco'dLeLb aNseEED TO KNOW ABOUT

MOAR Rebasing We have briefly talked about rebasing as being able to realign where the branch point exists for one branch onto another. In other words, you make history linear. Here is a simple visual example E - F - G topic / A - B - C - D master ➜ some-git git:(topic) git rebase master # we are on branch topic Assuming everything went off without a hitch, you will have the following state E - F - G topic

/ A - B - C - D master Interactive Rebasing and Squashing You may find yourself on a team that asks you to "squash" your commits. What is meant by this is interactive rebase squash. In other words: the aforementioned diagram we can transform from E - F - G topic / A - B - C - D master

To

  1. notice this is one commit EFG topic / A - B - C - D master Along with squashing, interactive rebasing allows you to edit messages and more Lets create a situation where we can interactively squash our commits and provide some proper messaging! But to get there we need to cover a LOT of ground

For us to cover this... we have to talk about the most dreaded topic in git Conflicts I hate them You hate them but its good to know how to resolve them. PLEASE A note for all who have a nice git plugin to make this process easier. Please do not use any fancy tools, lets just manually resolve these.

To create a conflict The easiest way to create a conflict is when you have two changes to a repo that cannot be resolved by the merging strategies. In other words, edit the same line.

Problem Create a conflict with remote-git and hello-git . To do this, please create a commit in both hello-git and remote-git editing the same location within a file. To accomplish this

  1. Use trunk in both repos
  2. change hello-git 's README.md first line to A + 1 and commit
  3. change remote-git 's README.md first line to A + 2 and commit
  4. pull hello-git into remote-git to create the conflict Solution
  5. Changed hello-git trunk 's README.md's first line cd path/to/hello-git ➜ hello-git git:(trunk) vim README.md

A + 1 D E remote-change Commit the change ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 1' [trunk 9648be0] A + 1 1 file changed, 1 insertion(+), 1 deletion(-) 2. Changed remote-git trunk 's README.md's first line cd path/to/remote-git ➜ remote-git git:(trunk) vim README.md A + 2 D E remote-change Commit the change

➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 2' [trunk 6eb0a42] A + 2 1 file changed, 1 insertion(+), 1 deletion(-) Time to pull down change from remote ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 303 bytes | 303.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 8058c68...9648be0 trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md Automatic merge failed; fix conflicts and then commit the result. We are now officially conflicted!

What is in conflict? Often its not obvious what is in conflict just by the message (if there is a large set of changes). So a simple way to see what is conflicted is by checking out the status ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Note There are some things to take from this status message

  1. Unmerged path's contains README.md and it says both modified . That is your key to what needs to be resolved
  2. You can abort the merge due to the conflict by executing git merge --abort Resolving a Conflict When you cat out the file that is conflicted, README.md , you will see some additional information in the file that was not there before ➜ remote-git git:(trunk) vim README.md
  3. This is the look of vim

1 <<<<<<< HEAD 1 A + 2 2 ======= 3 A + 1 4 >>>>>>> 9648be0ae764528ac63759d7e49fc623ae0af373 5 D 6 E 7 remote-change 8 downstream change So some important information is present.

  1. Any >>>>, ======, <<<<< denote parts of the conflict. 1 <<<<<<< HEAD This stats that HEAD s conflicted change starts here and continues until the ======= line. You can confirm this with git log -p -1 You can verify this by noticing that the change in the HEAD section is A + 2 which is the change that is in the remote-git trunk branch and is the HEAD location of remote-git

======= denotes the separation of the two merges The end of the merge conflict is denoted with >>>> and sha of the incoming conflicted change

9648be0ae764528ac63759d7e49fc623ae0af373 Problem Validate that the sha, mine is 9648be0ae764528ac63759d7e49fc623ae0af373, belongs to hello-git

Solution You can validate the bottom sha belonging to hello-git by using the following log ➜ hello-git git:(trunk) git log --oneline -1 9648be0 (HEAD -> trunk) A + 1 -1 with git log says only show 1 commit. -3 would show 3 commits of history. Notice that the hash provided in the conflict is HEAD in hello-git and it also matches the change of A + 1

Problem We are conflicted and we need to resolve this. Use the status message to identify which file to edit and what to do after you edit the file. Lets choose a side to keep as part of the merge. We will choose my remotegit change. To choose that commit, delete line <<<<<<< HEAD and delete from ======= up to and including >>>>>>> 9648be0ae764528ac63759d7e49fc623ae0af373 In other words we are keeping the HEAD changes and dropping the 9648be0 changes (for the sake of the course you should choose the same side) After conflict has been resolved (by removing the conflict markers and the code from hello-git ) commit the merge.

Before you commit the merge check the status Solution The desired code state should be: A + 2 D E remote-change downstream change We removed all the < , = , and > lines (conflict markers) and A + 1

(change from hello-git ) Side Note Now there is technically nothing preventing you from choosing both sides, and if you did that your code would look like A + 1 A + 2 D E remote-change downstream change git status tells us the next step ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

We need to run git commit ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git status On branch trunk All conflicts fixed but you are still merging. (use "git commit" to conclude merge) ➜ remote-git git:(trunk) git commit [trunk d8a2f95] Merge branch 'trunk' of ../hello-git into trunk

  1. You will be presented with this commit Merge branch 'trunk' of ../hello-git into trunk
  2. Conflicts:
  3. README.md

  1. It looks like you may be committing a merge.
  2. If this is not correct, please run
  3. git update-ref -d MERGE_HEAD
  4. and try again.
  5. Please enter the commit message for your changes. Lines starting
  6. with '#' will be ignored, and an empty message aborts the commit.

  1. On branch trunk

  2. All conflicts fixed but you are still merging.

Thought Exercise There was no status... why?

Solution We didn't accept the changes from hello-git . We effectively deleted all the changes from remote so the change was empty. There is still a merge commit that is needed. You can validate the merge commit by a quick look at the logs d8a2f95 (HEAD -> trunk) Merge branch 'trunk' of ../hello-git into trunk 9648be0 (origin/trunk) A + 1 6eb0a42 A + 2 7282922 greatest changes 6849c67 upstream changes 42afc8d A remote change b23e632 Y 2f43452 X a665b08 E 79c5076 D cb75afe A Notice that we have a merge commit and we also have A + 1 commit. The history is not lost, but the changes are not present Try looking at the log with --graph

Problem Two conflicts are better than one, right? .... right? Ok, i agree. Lets not conflict again. Instead

  1. create a change in bar.md in hello-git ➜ hello-git git:(trunk) echo "no conflict" >> bar.md
  2. pull in change in remote

Solution Make the change ➜ hello-git git:(trunk) echo "no conflict" >> bar.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'no conflict' [trunk a9cb358] no conflict 1 file changed, 1 insertion(+) Pull in the change to remote-git ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done.

remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 297 bytes | 27.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 9ee733d...a9cb358 trunk -> origin/trunk (forced update) Merge made by the 'ort' strategy. bar.md | 1 + 1 file changed, 1 insertion(+) 1 Merge branch 'trunk' of ../hello-git into trunk 1 # Please enter a commit message to explain why this merge is necessary

2 # especially if it merges an updated upstream into a topic branch. 3 # 4 # Lines starting with '#' will be ignored, and an empty message aborts 5 # the commit. Observation Notice you do have to have another merge commit because the origin does not contain your commits, so every merge will cause a merge commit. That means you could get a pretty hairy set of commits. Check out the graph again with log --graph

Takeaway? • once you resolve a conflict and you don't take upstream's you will get merge commits until you sync your changes back to the remote Conflicts, but with rebase aren't you excited for more conflicts?

Problem What is unique about rebase that would make conflicts harder? Solution

Recall that rebase will replay all your commits after moving forward the history, which means what if a conflict happened in the past? Lets say you have the following setup: E - F - G topic / A - B - C - D master And lets pretend that C contains a change that creates a conflict with G . We rebase and we resolve the conflict and now our graph looks like the following: E - F - G topic / A - B - C - D master Then master gets another commit, Y

E - F - G topic / A - B - C - D - Y master Now if we rebase again, we will play E , F , and G . Since we are computer scientists, aka masochists, Lets do this to ourselves! Problem To ensure everything continues on going smooth, lets update trunk in hello-git with push from hello-git We don't want more merge commits...

Solution ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 15, done. Counting objects: 100% (14/14), done. Delta compression using up to 12 threads Compressing objects: 100% (7/7), done. Writing objects: 100% (9/9), 1.07 KiB | 1.07 MiB/s, done. Total 9 (delta 1), reused 0 (delta 0), pack-reused 0 remote: error: refusing to update checked out branch: refs/heads/trunk remote: error: By default, updating the current branch in a non-bare reposit remote: is denied, because it will make the index and work tree inconsistent remote: with what you pushed, and will require 'git reset --hard' remote: the work tree to HEAD. remote: remote: You can set the 'receive.denyCurrentBranch' configuration variable remote: to 'ignore' or 'warn' in the remote repository to allow pushing remote: its current branch; however, this is not recommended unless you remote: arranged to update its work tree to match what you pushed remote: other way. remote:

remote: To squelch this message and still keep the default behaviour, remote: 'receive.denyCurrentBranch' configuration variable to To ../hello-git ! [remote rejected] trunk -> trunk (branch is currently checked out) error: failed to push some refs to '../hello-git' What happened here? Observation ... ! [remote rejected] trunk -> trunk (branch is currently checked out) We cannot push to a branch that is the current branch of the target repo. This makes sense as it would cause your current branch to change out of underneath the repo that is currently being used, and if there are pending changes it could cause further havoc.

What do we do? Change branches! ➜ hello-git git:(trunk) git checkout bar Switched to branch 'bar' Now lets try again ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 15, done. Counting objects: 100% (14/14), done. Delta compression using up to 12 threads Compressing objects: 100% (7/7), done.

Writing objects: 100% (9/9), 1.07 KiB | 1.07 MiB/s, done. Total 9 (delta 1), reused 0 (delta 0), pack-reused 0 To ../hello-git a9cb358..b51e34a trunk -> trunk Problem Lets create another conflict but resolve this via rebase instead of merge .

  1. create change in hello-git and A + 2 -> A + 3 .
  2. Create another change in bar.md LAST LINE in hello-git
  3. create change in remote-git and A + 2 -> A + 4
  4. Create another change in bar.md FIRST LINE in remote-git
  5. rebase remote-git 's trunk with hello-git 's and create the conflict

Solution happy-git ➜ hello-git git:(bar) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) vim bar.md ➜ hello-git git:(trunk) git diff diff --git a/README.md b/README.md index e42f7f7..43b4231 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 2 +A + 3 D E remote-change diff --git a/bar.md b/bar.md index 04fca9f..894d904 100644 --- a/bar.md +++ b/bar.md @@ -1,3 +1,4 @@ X

Y no conflict +adding a line to the end ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 3' [trunk 958f33f] A + 3 2 files changed, 2 insertions(+), 1 deletion(-) remote-git ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) vim bar.md ➜ remote-git git:(trunk) git diff diff --git a/README.md b/README.md index e42f7f7..76c0a5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 2 +A + 4 D E remote-change diff --git a/bar.md b/bar.md index 04fca9f..3dc9259 100644 --- a/bar.md +++ b/bar.md @@ -1,3 +1,4 @@ +first line change X Y no conflict ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 4' [trunk a1cbe6c] A + 4 2 files changed, 2 insertions(+), 1 deletion(-) Now that we are all primed and ready for a conflict.

➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 7, done. remote: Counting objects: 100% (7/7), done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (4/4), 367 bytes | 367.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 75e4992...958f33f trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md Auto-merging bar.md error: could not apply a1cbe6c... A + 4 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply a1cbe6c... A + 4 NOTICE If you read carefully you will see that bar was able to be Auto-merged

where as README was not able to be merged NOTICE We can git rebase --abort due to the conflict (much like the git merge --abort ). NOTICE This is important. Once you have resolved the conflict we need to git rebase --continue instead of git commit . GIT FU • If you did use git reset --soft HEAD~1 and then git rebase --continue

Checking out the conflict ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md You will notice that bar.md is marked (green if you have coloring) committed while README.md is unmerged. Lets fix our conflict in README.md Opening up README.md shows us the following

Thought Exercise Explain the conflict and why its different than merge A + 3 D E remote-change downstream change Answer

This was pretty tricky question. noticed that A + 3, from hello-git now takes the top spot and A + 4 takes the bottom. What are the steps of rebase? Problem Choose our conflict, hello-git 's change. (A + 3) Remember do not commit. We git rebase --continue . This signals to the rebase command that we are ready for the next commit to be played on top.

Solution ➜ remote-git git:(958f33f) git add . ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (all conflicts fixed: run "git rebase --continue") Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md ➜ remote-git git:(958f33f) git rebase --continue [detached HEAD c7b9731] A + 4 1 file changed, 1 insertion(+) Successfully rebased and updated refs/heads/trunk. A + 4

  1. Please enter the commit message for your changes. Lines starting
  2. with '#' will be ignored, and an empty message aborts the commit.

  1. interactive rebase in progress; onto 958f33f

  2. Last command done (1 command done):

  3. pick a1cbe6c A + 4

  4. No commands remaining.

  5. You are currently rebasing branch 'trunk' on '958f33f'.

  1. Changes to be committed:
  2. modified: bar.md

Since there are no more commits that cause conflicts the rebase is complete. Lets take a quick look at our logs Exercise check out the history

Result ➜ remote-git git:(trunk) git log --oneline -5 c7b9731 (HEAD -> trunk) A + 4 958f33f (origin/trunk) A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk a9cb358 no conflict d8a2f95 Merge branch 'trunk' of ../hello-git into trunk You will see our A + 4 and you will see origin's A + 3 "underneath" or previous in history. Note There is no merge commit. People really seem to like this cleaner history.

The Problem of Rebase Don't forget, rebase replays the commits on top of the history change. Here is the linked git status call from above ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (all conflicts fixed: run "git rebase --continue") Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md Thought Experiment Ask yourself, where is README.md?

Answer Well, since we accepted theirs (marked as ours in the resolution) (we will talk about ours vs theirs later) changeset, we technically removed any change from README.md . Therefore there was no change to rebase continue on. Remember, we have origin/trunk effectively checked out. therefore there is no change to README.md when we accept our changes during a rebase Problem Lets try create the same issue except this time lets accept ours ( theirs by

position (bottom)) change.

  1. A + 5 in hello-git
  2. A + 6 in remote-git
  3. git pull origin trunk --rebase in remote-git to cause the conflict
  4. accept A + 6 change and git rebase --continue
  5. check out history to see A + 6 commit Solution remote-git ➜ remote-git git:(trunk) git diff diff --git a/README.md b/README.md index 43b4231..0c72736 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 3 +A + 5

D E remote-change ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 6' [trunk 6740bc7] A + 5 1 file changed, 1 insertion(+), 1 deletion(-) hello-git ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 5' [trunk fac2b82] A + 6 1 file changed, 1 insertion(+), 1 deletion(-) rebase ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 094cca8...fac2b82 trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 6740bc7... A + 5 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply 6740bc7... A + 5 ➜ remote-git git:(6a3fb28) vim README.md

➜ remote-git git:(6a3fb28) git status interactive rebase in progress; onto fac2b82 Last commands done (2 commands done): pick c7b9731 A + 4 pick 6740bc7 A + 5 No commands remaining. You are currently rebasing branch 'trunk' on 'fac2b82'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(6a3fb28) git add . ➜ remote-git git:(6a3fb28) git rebase --continue A + 5

  1. Please enter the commit message for your changes. Lines starting
  2. with '#' will be ignored, and an empty message aborts the commit.

  1. interactive rebase in progress; onto fac2b82
  2. Last commands done (2 commands done):
  3. pick c7b9731 A + 4
  4. pick 6740bc7 A + 5
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on 'fac2b82'.

  1. Changes to be committed:
  2. modified: README.md

[detached HEAD 52bfa5a] A + 5 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated refs/heads/trunk.

check history ➜ remote-git git:(6a3fb28) git log --oneline -5 52bfa5a (HEAD -> trunk) A + 5 6a3fb28 A + 4 fac2b82 (origin/trunk) A + 6 958f33f A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk This is where rebase can suck This is where things get complicated. We have kept our change in the rebase. (our being remote-git's trunk) Problem Create a change in hello-git and pull again.

  1. Add a NewLine below A + 6 in hello-git
  2. rebase pull hello-git and cause the conflict
  3. DO NOT RESOLVE THE CONFLICT

Solution ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git diff diff --git a/README.md b/README.md index c04f8e6..18b7811 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ A + 6 +NewLine D E remote-change ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'small new line' [trunk 99df23c] small new line 1 file changed, 1 insertion(+) Now lets pull from remote-git with rebase

➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 328 bytes | 328.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

fac2b82..99df23c trunk -> origin/trunk Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 52bfa5a... A + 5 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply 52bfa5a... A + 5 Weird... lets check the conflict ➜ remote-git git:(abcc4f4) git status interactive rebase in progress; onto 99df23c Last commands done (2 commands done): pick 6a3fb28 A + 4 pick 52bfa5a A + 5 No commands remaining. You are currently rebasing branch 'trunk' on '99df23c'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

Ok, only README.md is conflicted. A + 6 NewLine D E remote-change downstream change Wait... didn't we already resolve this conflict? Why are we resolving it again? Thought Exercise Does this now make sense with how rebase works?

Answer Rebase works by replaying the commits one at a time. Therefore if we have our change from a conflict and then we replay the changes we will reconflict on the same change again and again. Does that mean rebase sucks? Well no, it keeps your history very clean, but does that mean rebase can be annoying? Yes. Question (i know there is an active conflict right now due to rebase)

Given what you know now, would you use rebase or merge? Answer I would... and there is likely something you don't know about

RERERE rerere is just one of the strangest options in all of git. There are some basic commands that can be ran. Check out the man page, man git-rerere to go into details. I have never needed them before, i just use the config and live my best life From the The Git Docs The git rerere functionality is a bit of a hidden feature. The name stands for “reuse recorded resolution” and, as the name implies, it allows you to ask Git to remember how you’ve resolved a hunk conflict so that the next time it sees the same conflict, Git can resolve it for you automatically. Problem

Enable rerere just for this project. You can enable it globally if you like it. Make the config option rerer.enabled to true Solution git config rerere.enabled true and to validate git config --list --local ... a few options ... rerere.enabled=true

What is rerere? rerere stands for REuse REcorded REsolution. Or in other words, git will automagically remember how you handled a specific conflict and will just replay your decision the next time you run into it. It is not all sunshine and rainbows. You can, refer to man git-rerere , delete rerere's in case you incorrectly resolved a conflict

Problem Resolve the conflict and accept our change, A + 6 Solution ➜ remote-git git:(abcc4f4) vim README.md ➜ remote-git git:(abcc4f4) git status interactive rebase in progress; onto 99df23c Last commands done (2 commands done): pick 6a3fb28 A + 4 pick 52bfa5a A + 5 No commands remaining. You are currently rebasing branch 'trunk' on '99df23c'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage)

(use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(abcc4f4) git add . ➜ remote-git git:(abcc4f4) git rebase --continue [detached HEAD 226add3] A + 5 1 file changed, 1 insertion(+), 2 deletions(-) Successfully rebased and updated refs/heads/trunk. Problem To test out our rerere, lets create one more change to hello-git and see if we can auto play the conflict resolution For this add a small change to upstream.md .

Solution ➜ hello-git git:(trunk) vim upstream.md ➜ hello-git git:(trunk) git diff [trunk 980fe2d] yaya 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upstream.md b/upstream.md index b39813a..5691d02 100644 --- a/upstream.md +++ b/upstream.md @@ -1 +1 @@ -upstream change +upstream change -- yaya ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'yaya' now in remote-git ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 248 bytes | 248.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

99df23c..980fe2d trunk -> origin/trunk ➜ remote-git git:(trunk) git log --oneline -5 Successfully rebased and updated refs/heads/trunk. c28b45c (HEAD -> trunk) A + 5 2107110 A + 4 980fe2d (origin/trunk) yaya 99df23c small new line fac2b82 A + 6 Lets go! Our conflict this time was auto played for us! Ours and Theirs Sometimes during a conflict you just want to choose the entire file from one side or the other of a conflict. Typically in an editor this is a very simple task, but it can as easily be done from the command line.

Ours and Theirs "Its just the worst with git" - Some Coding Guy Ours Vs Theirs • Ours is the change of the current branch

• Theirs is the change of the incomming branch To select theirs or ours use the following checkout command git checkout --ours README.md #use "ours" change git checkout --theirs README.md #use "theirs" change Problem / Setup Lets create a conflict again, in README.md

  1. hello-git make it A + 7
  2. remote-git make it A + 8
  3. merge from upstream and resolved the conflict with "ours"
  4. validate you have merged the changes via git log

Solution ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 8' [trunk 6ec352b] A + 7 1 file changed, 1 insertion(+), 1 deletion(-) ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 7' [trunk f5b13f5] A + 8 1 file changed, 1 insertion(+), 1 deletion(-) Lets do a merge instead of a rebase and pull from origin in remote-git ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 323 bytes | 323.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

980fe2d..f5b13f5 trunk -> origin/trunk Auto-merging README.md

CONFLICT (content): Merge conflict in README.md Recorded preimage for 'README.md' Automatic merge failed; fix conflicts and then commit the result. Now, lets resolve this by selecting our change. ➜ remote-git git:(trunk) git checkout --ours README.md Updated 1 path from the index ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m "merged" Recorded resolution for 'README.md'. [trunk ec6930d] merged We have now officially "merged" with ours, and you can verify this by opening up README.md and looking at the contents. A + 7 D E remote-change downstream change You can even see the change in the log for A + 8, but we still maintain 7 ➜ remote-git git:(trunk) git log --oneline -5 --graph

ec6930d (HEAD -> trunk) merged

|
| * f5b13f5 (origin/trunk) A + 8

| 6ec352b A + 7

| c28b45c A + 5

| 2107110 A + 4

|/ A good reminder

  1. you don't want to mix merge and rebase. I typically just try to stick with rebasing my branch and ff-merge on public branches.
  2. long lived feature branches just suck. rerere helps, but they still suck Lets do this again, but use rebase. But before we do, since have mixed merge and rebase, lets push our changes to hello-git or else things will get hairy quickly.

Don't forget Don't forget to change branches in hello-git ➜ hello-git git:(trunk) git checkout bar Switched to branch 'bar' push to hello-git ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 16, done. Counting objects: 100% (15/15), done. Delta compression using up to 12 threads Compressing objects: 100% (8/8), done. Writing objects: 100% (10/10), 1.05 KiB | 1.05 MiB/s, done. Total 10 (delta 1), reused 0 (delta 0), pack-reused 0 To ../hello-git f5b13f5..ec6930d trunk -> trunk

Problem Perform the same task as before except with rebase

  1. hello-git make it A + 9
  2. remote-git make it A + 10
  3. rebase from upstream and resolved the conflict with "ours."
  4. do not git rebase --continue Solution remote-git

➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 10' [trunk 120df5e] A + 9 1 file changed, 1 insertion(+), 1 deletion(-) hello-git ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 9' [trunk d53a122] A + 10 1 file changed, 1 insertion(+), 1 deletion(-) Now lets pull again from remote but with --rebase enabled ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

ec6930d..d53a122 trunk -> origin/trunk Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 120df5e... A + 9 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Recorded preimage for 'README.md' Could not apply 120df5e... A + 9 Perfect, we now can select ours via git checkout

➜ remote-git git:(d53a122) git checkout --ours README.md Updated 1 path from the index Before we commit, lets take a look at the contents A + 9 D E remote-change downstream change wait... a second. That isn't the contents we wanted! We wanted ours. What happened? Note Well remember the rebase operations

  1. checkout the branch we are rebasing on. (that means checkout hello git)
  2. replay our changes on top of that updated branch one at a time

So that means when we hit a conflict: • ours IS hello-git , the branch we are replaying on. • theirs IS remote-git , the commits we are replaying one at a time. It is the easiest part of git to screw up and i personally remember it as "rebase is backwards." Or you can remember the steps of rebase and come to the same conclusion. Problem Instead of using ours use theirs then --continue the rebase.

Solution ➜ remote-git git:(d53a122) git checkout --theirs README.md Updated 1 path from the index Now lets inspect the contents A + 10 D E remote-change downstream change nice git add && rebase --continue to move on

Interactive Rebase There is ackshually more to rebase? Yes. Lets talk about interactive rebases, which are quite useful. The primary use case is squashing which can be very nice for history you can also edit individual commit messages and more, but i haven't ever really done that in my 10+ years of git'ing Problem Setup the repo with 3 sample commits on remote-git . To make things easy, make the commit message something like "added 1 to the end" and add 1 to the end of README.md

Solution ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 1 to the end' [trunk 9ebedbd] Added 1 to the end 1 file changed, 1 insertion(+) ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 2 to the end' [trunk 8456d89] Added 2 to the end 1 file changed, 1 insertion(+) ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 3 to the end' [trunk f000c2e] Added 3 to the end 1 file changed, 1 insertion(+) The contents of README.md A + 9 D E remote-change

downstream change 1 2 3 Interactive Rebase Steps To begin an interactive rebase we need to provide a point in time to rebase with. Typically, the simplest way to do this is with HEAD . HEAD1 means one commit back from HEAD . Since we did 3 commits we would use HEAD~3 to select the base where we were before our 3 commits. git rebase -i That means rebase , interactively, to the current commit

( HEAD in this case) You will be presented an editor with all the options. Read them carefully. Commitish? Yes, an odd word, but it makes sense when you think about it. There is a whole language to describe commits, HEAD1 is a very common version of this. That means with rebase you could provide the exact commit, or a relative path to the commit hash (HEAD1) Problem

SQUASH the three commits you made into one commit. This will require you to execute the rebase command, a HEAD , and read the text that appears to understand how to squash. It may take one or more tries. Remember, if you goof up you can always use reflog to get back to the original commit you started at. validate that you have created a squashed commit out of the 3 commits Solution execute the following: git rebase -i HEAD3

This means we will interactive rebase the last 3 commits. You should get presented with the following pick 9ebedbd Added 1 to the end pick 8456d89 Added 2 to the end pick f000c2e Added 3 to the end

  1. Rebase 9f67690..f000c2e onto 9f67690 (3 commands)

  1. Commands:
  2. p, pick = use commit
  3. r, reword = use commit, but edit the commit message
  4. e, edit = use commit, but stop for amending
  5. s, squash = use commit, but meld into previous commit
  6. f, fixup [-C | -c] = like "squash" but keep only the previous
  7. commit's log message, unless -C is used, in which # keep only this commit's message; -c is same as -C # opens the editor
  8. x, exec = run command (the rest of the line) using shell
  9. b, break = stop here (continue rebase later with 'git rebase --continue')
  10. d, drop = remove commit
  11. l, label
  12. t, reset
  13. m, merge [-C | -c ]
  14. . create a merge commit using the original merge commit's
  15. . message (or the oneline, if no original merge commit was
  16. . specified); use -c to reword the commit message

  1. These lines can be re-ordered; they are executed from top to bottom.

  1. If you remove a line here THAT COMMIT WILL BE LOST.

  1. However, if you remove everything, the rebase will be aborted.

The key line in the text comments is

  1. s, squash = use commit, but meld into previous commit This means that if we replace pick with s or squash we will squash that commit, or meld into previous commit . Meaning make the previous commit and squash commit become one commit. pick 9ebedbd Added 1 to the end squash 8456d89 Added 2 to the end squash f000c2e Added 3 to the end We could have also done pick 9ebedbd Added 1 to the end s 8456d89 Added 2 to the end s f000c2e Added 3 to the end Save and exit and git will present a new screen
  2. This is a combination of 3 commits.
  3. This is the 1st commit message:

Added 1 to the end

  1. This is the commit message #2: Added 2 to the end
  2. This is the commit message #3: Added 3 to the end
  3. Please enter the commit message for your changes. Lines starting
  4. with '#' will be ignored, and an empty message aborts the commit.

  1. Date: Sun Feb 25 08:50:40 2024 -0700

  1. interactive rebase in progress; onto 9f67690
  2. Last commands done (3 commands done):
  3. squash 8456d89 Added 2 to the end
  4. squash f000c2e Added 3 to the end
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on '9f67690'.

  1. Changes to be committed:
  2. modified: README.md

Now you have the chance to create a whole new commit message for the newly combined commits. Lets edit the message slightly

  1. This is a combination of 3 commits.

  2. This is the 1st commit message: 1, 2, and 3 combined

  3. Please enter the commit message for your changes. Lines starting

  4. with '#' will be ignored, and an empty message aborts the commit.

  1. Date: Sun Feb 25 08:50:40 2024 -0700

  1. interactive rebase in progress; onto 9f67690
  2. Last commands done (3 commands done):
  3. squash 8456d89 Added 2 to the end
  4. squash f000c2e Added 3 to the end
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on '9f67690'.

  1. Changes to be committed:
  2. modified: README.md

Once you save, you should see something similar: ➜ remote-git git:(trunk) git rebase -i HEAD~3 [detached HEAD 02d3a0f] 1, 2, and 3 combined Date: Sun Feb 25 08:50:40 2024 -0700 1 file changed, 3 insertions(+) Successfully rebased and updated refs/heads/trunk. Lets look at our logs ➜ remote-git git:(trunk) git log --oneline -7 02d3a0f (HEAD -> trunk) 1, 2, and 3 combined

9f67690 A + 9 d53a122 (origin/trunk) A + 10 ec6930d merged f5b13f5 A + 8 6ec352b A + 7 c28b45c A + 5 Look at that! Our three commits became one! Squashing can be quite an effective technique to keep the history clean and allow you to make many small commits throughout your dev cycle, preventing loss work, and then one clean commit for reviewers. I personally think this is one of the best ways to go about developing. My general workflow

  1. many small commits with a message "SQUASHME: "
  2. at the end of the dev cycle, i squash and give a proper message
  3. PR with a singular commit

← Previous Next → 4. before i PR i ensure i am at the tip of the branch and that any CI runs against latest EVERYTHING YOU'L Lm NasEteEr Dch TanOg eKsNOW

The repo needing to be downloaded git clone git@github.com:ThePrimeagen/git-bisect.git A Classic Problem • somewhere in the last 500 commits something has gone wrong. • To test if something has gone wrong takes several minutes or longer. This is not a common problem, but it is a problem you will run into in the real world. And it can be a complete pain to resolve if you do not know the tools you have at your disposal.

Logs One very straight forward strategy to determine where a bug began is manually reviewing logs and identifying when a file changed. Pros • If you know the file/module in which the bug exists and the file changes infrequently then logs can be a very fast method to identify the problem commit. ◦ This can be particularly useful when people use good commit messages. It will help you understand why they made the change they did • If people take seriously their commit messages and add key words then searching can provide a fast and powerful mechanism to find the correct change. Cons

• The file/module in which contains the bug changes frequently • Poor/No commit messages • You cannot boil down the bug to a specific key word (e.g. Widget) • If there are too many commits that match searching it can much more cumbersome than using other methods • You simply don't know any keywords to narrow down your search Searching with token Prepare Repo Before you begin with the exercise it will be good to be familiar with the repo.

  1. install any deps with npm i
  2. run tests with npm run test

Results You should see something similar if everything went well ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1)

Start at 15:46:36 Duration 30.46s (transform 44ms, setup 0ms, collect 18ms, tests 30.11s, environment 0ms, prepare 107ms) Observation we know our function foo and test foo are failing git log Problem Use man git-log and search for an option to search through logs. A bonus info diffs of the logs could be useful too!

hint: often search is referred to as grep Solution git log comes with a --grep option git log --grep "" This will search through the commits and look in the commit message for . Diff option -p will show the commits diff as well

Problem Can you find the wrong commit using --grep ? Solution ➜ git-bisect git:(master) git log -p --grep "foo" Shockingly, we have spotted our code. commit 3798398f377cd1722284ff30b211e3b66e218738

Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications diff --git a/src/index.js b/src/index.js index 4f35883..e8052cc 100644 --- a/src/index.js +++ b/src/index.js @@ -4,5 +4,5 @@

@return {number}

*/ export function foo(x) {

  • return x * 69;
  • return x * 70; } Now this is a "lucky" search. We happened to find it but in the real world this may not be nearly so easy. But its good to know the tool exists.

You could image that each testing run takes 30 minutes, so spending 5 minutes to potentially find the bug instead of 3 hours is a trade off i would make any day of the week. Of course this also assumes you know exactly which file has fallen apart, and more specifically for us, there isn't a simple test to fix :) Searching with filename One flaw in our previous search is that we were looking for occurrences of a word in commit messages which may not be the most efficient way to look. We know the file that is probably the problem, we could always check its history. To search files via git log

git log -p -- file1 file2... Problem Use file mechanism in git log to see if it helps the search

Solution git log -p -- src/index.js You may not notice, but this method only shows changes to that specific file instead of the full patch change. This allows for a bit faster perusing of the file and its change. Again, in this trivial example, it is easy to spot the offending commit! Note A nice part about file changes is that sometimes you may find a bug was caused due to another bug, a hydrabug. Perhaps there was a production issue and a jira ticket linked to a change that is now causing a new issue. History can be nice to search through.

Your Batgit belt Its good to have these in your back pocket even if you don't use them for years. There is always that some point in the future where all the sudden this will be very useful. The nice part is that all the information you need is in man git-log -S allows you to search for text in the change itself! Bisect But log searching just simply may fail or the code is complex enough that it isn't possible to simply look and understand. So what do we do? Well we need to search through the repository for the offending commit that changed the code.

To perform bisect you need two things to be true Property 1 All commits are ordered. They are ordered by time. Property 2 You know a commit that the issue is not present or can find it easily enough The reason property 1 is so important is the following: • If there is a problem that is currently plaguing the project and you go back 10 commits and observe the problem is still there. You don't have to check 9 commits back, or 8 back, you know that the problem currently exists and 10 commits ago its present. Therefore its present betwixt HEAD and 10 commits ago

This implies a very important concept

| | a ---------- unknown ----------- b If a is working commit, and b doesn't work, select the middle commit

between a and b , call it c

| | a -- unknown -- c -- unknown -- b

Problem given:

| | a -- unknown -- c -- unknown -- b if c turns out to be a commit that passes the test (it works) what observation can we make about the above graphic? Solution All commits between a and c are good. Which results in the following

graph

| | a --- good --- c --- unknown --- b

Problem given

| | a -- unknown -- c -- unknown -- b If c fails, what observations can we made about the original graph?

Solution All commits between c and b are bad. Which results in the following graph

| | a --- unknown --- c --- bad --- b Observation we can repeat this process to find our offending commit that broke the test! To repeat we need to select new bounds. So instead of a and b , we repeat with the bounds of

• if c is good, c and b • if c is bad, a and c We have cut our search space in half! If you don't recognize this algorithm it is the same principal that guides binary search. Free Algorithms Course I consider this my best course i have ever created and its free FOREVER Algorithms on FEM

Pros • You need not to know anything about the bug and you don't have to rely on commit messages. Simply a failing test case or a way to reproduce the bug manually or programmatically • It is the fastest way to search a sorted space • You don't have to be searching only for a bug but for any change in your project. You can also use this to find where a bug got fixed, where a performance regression happened, or anything else you can think of. Cons • not really any... Unless its a trivial bug that works via log searching this is pretty much the best possible option Performing bisect Git bisect requires you to have a last known good commit and a last known bad commit. From there it is able to perform the binary search.

The last known bad commit doesn't have to be best fit and neither does the last known good commit. The point is that bisect does the searching, not you. O(log n) • 1 = 1 search • 2 = 1 search • 4 = 2 searches • 8 = 3 searches • 16 = 4 searches • 32 = 5 searches • 64 = 6 searches • 128 = 7 searches ... The Basics of Bisect

  1. start git bisect git bisect start

  2. set the known bad commit git bisect bad , uses the current one

  3. set the known good commit git bisect good

  4. test

  5. git bisect <good | bad> depending on how the test runs

  6. goto 4 until git tells you the commit Problem Use the first commit of the repo and the current tip of master and find the problematic commit via git bisect. Solution

  7. Starts the process ➜ git-bisect git:(master) git bisect start

  8. Sets current commit as the bad commit. You can manually select any commit ➜ git-bisect git:(master) git bisect bad

  9. Find the initial commit ➜ git-bisect git:(master) git log --oneline

  10. Set the last known good commit to the first commit

➜ git-bisect git:(master) git bisect good b56ed57

  1. Test to see if we are working or not ➜ git-bisect git:(master) npm run test ... Test Files 1 failed (1) Tests 1 failed (1) Start at 10:49:35 Duration 30.34s (transform 24ms, setup 1ms, collect 9ms, tests 30.11s, Looks like we have a bad commit so lets execute the following:
  2. This tells git that we have a bad commit ➜ git-bisect git:(8bf2c77) git bisect bad Bisecting: 5 revisions left to test after this (roughly 3 steps) [0562cd710d40d23b5928747a45d71d2a76443752] F
  3. notice that my shell shows i have changed commits ➜ git-bisect git:(0562cd7) Lets test again! Keep on testing until we have found the commit. ✓ src/index.spec.js (1) 30041ms ✓ foo 30040ms Test Files 1 passed (1) Tests 1 passed (1) Start at 10:53:21 Duration 30.26s (transform 26ms, setup 0ms, collect 12ms, tests 30.04s, Ok we have made great success, therefore we need to tell git that we are successful. ➜ git-bisect git:(0562cd7) git bisect good Bisecting: 2 revisions left to test after this (roughly 2 steps) [eb867402c2e4678887c8aa322c0da3ef54851f7c] I

➜ git-bisect git:(eb86740) Again, git checks out the next correct branch. Lets keep running until we find the offending commit. Once you have successfully ran to the end, this is what you should see. ➜ git-bisect git:(3798398) git bisect bad 3798398f377cd1722284ff30b211e3b66e218738 is the first bad commit commit 3798398f377cd1722284ff30b211e3b66e218738 Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) And that is the offending commit! To finish up with your session, execute the following command. ➜ git-bisect git:(3798398) git bisect reset Previous HEAD position was 3798398 feat: altered foo to meet new specificati Switched to branch 'master'

Git Bisect - Automated Git bisect is great, but its a bit manual... right? If only there was a way to automate it In steps git bisect run . Effectively the exit code

  1. same setup as before with start, good, bad git bisect run This will continue to run until we find the proper commit. The run command uses the command provided and determines a good and bad commit by exit code Problem

Use git bisect run ./node_modules/.bin/vitest --run to automate the testing and find the same problem commit Solution ➜ git-bisect git:(master) git bisect start ➜ git-bisect git:(master) git log --oneline ➜ git-bisect git:(master) git bisect good b56ed57 ➜ git-bisect git:(master) git bisect bad ➜ git-bisect git:(8bf2c77) git bisect run ./node_modules/.bin/vitest --running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30107ms × foo 30106ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1) Start at 20:15:58 Duration 30.54s (transform 45ms, setup 0ms, collect 13ms, tests 30.11s, Bisecting: 5 revisions left to test after this (roughly 3 steps) [0562cd710d40d23b5928747a45d71d2a76443752] F running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ✓ src/index.spec.js (1) 30101ms ✓ foo 30099ms Test Files 1 passed (1) Tests 1 passed (1) Start at 20:16:29 Duration 30.59s (transform 48ms, setup 0ms, collect 11ms, tests 30.10s, Bisecting: 2 revisions left to test after this (roughly 2 steps) [eb867402c2e4678887c8aa322c0da3ef54851f7c] I running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect

✓ src/index.spec.js (1) 30070ms ✓ foo 30068ms Test Files 1 passed (1) Tests 1 passed (1) Start at 20:17:00 Duration 30.59s (transform 76ms, setup 0ms, collect 13ms, tests 30.07s, Bisecting: 0 revisions left to test after this (roughly 1 step) [972fa2ab45e5041a1fe8c95f31b520bc62d7af85] this commit is certainly not running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30080ms × foo 30079ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1)

Tests 1 failed (1) Start at 20:17:31 Duration 30.60s (transform 106ms, setup 0ms, collect 18ms, tests 30.08s, Bisecting: 0 revisions left to test after this (roughly 0 steps) [3798398f377cd1722284ff30b211e3b66e218738] feat: altered foo to meet new running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30111ms × foo 30111ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1) Start at 20:18:02 Duration 30.57s (transform 27ms, setup 0ms, collect 9ms, tests 30.11s, 3798398f377cd1722284ff30b211e3b66e218738 is the first bad commit

← Previous Next → commit 3798398f377cd1722284ff30b211e3b66e218738 Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) bisect found first bad commit% ➜ git-bisect git:(3798398) What. That was easy!

The Decision We All Face Often you have one of two choices to make: push forward or roll back. Rolling back can often require you to revert changes made to the main branch. Note In case you are confused about revert and restore This is different than git restore since we are not restoring a file to a previous commit, but instead we are commiting an inverted commit to the graph to effectively "remove" a commit. If the commit is matter, git revert is the anti-matter

Git Revert Reverting is simple. You just need to provide the commit(ish). git revert Problem Navigate to hello-git project and revert commit with log message E . Before removing it, check the contents of the commit via log with -p !

Solution One thing we could do ➜ hello-git git:(trunk) git log -p --grep E d53a122 (HEAD -> trunk) A + 10 ec6930d merged f5b13f5 A + 8 6ec352b A + 7 c28b45c A + 5 2107110 A + 4 980fe2d yaya 99df23c small new line fac2b82 A + 6 958f33f A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk a9cb358 no conflict d8a2f95 Merge branch 'trunk' of ../hello-git into trunk 9648be0 A + 1 6eb0a42 A + 2 7282922 greatest changes 6849c67 upstream changes 42afc8d A remote change b23e632 Y 2f43452 X

a665b08 E 79c5076 D cb75afe A Using log -p to look at the contents of E using --grep and -p ➜ hello-git git:(trunk) git log -p --grep E commit a665b08996994c2e6620a6367b0ab524be221cb2 Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:56:37 2024 -0700 E diff --git a/README.md b/README.md index b2f0a9a..d3045e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ A D +E Revert E ➜ hello-git git:(trunk) git revert a665b08 Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not revert a665b08... E hint: After resolving the conflicts, mark them with hint: "git add/rm ", then run hint: "git revert --continue". hint: You can instead skip this commit with "git revert --skip" hint: To abort and get back to the state before "git revert" hint: run "git revert --abort".

HALT Yes, conflicts can happen while reverting. Lets fix the conflict and finish this revert. Resloving them are a lot like rebase. figure out the code you want to keep and git revert --continue ➜ hello-git git:(trunk) git status On branch trunk You are currently reverting commit a665b08. (fix conflicts and run "git revert --continue") (use "git revert --skip" to skip this patch) (use "git revert --abort" to cancel the revert operation) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" git status shows the conflict is within README.md

A + 10 D <<<<<<< HEAD E remote-change downstream change

parent of a665b08 (E) You can see right away that one change set contains E remote-change downstream change and the other contains nothing (our revert commit). This means that git cannot tell how to revert this commit cleanly. We can help by keeping the contents below E but removing E

Problem Finish the revert by fixing the conflict then use git revert --continue use git log to see what the revert looks like Solution Merge conflict fixed

A + 10 D remote-change downstream change Now, just like rebase, we have to git revert --continue ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git revert --continue Revert "E" This reverts commit a665b08996994c2e6620a6367b0ab524be221cb2.

  1. Conflicts:
  2. README.md
  3. Please enter the commit message for your changes. Lines starting
  4. with '#' will be ignored, and an empty message aborts the commit.

  1. On branch trunk
  2. You are currently reverting commit a665b08.

  1. Changes to be committed:
  2. modified: README.md

[trunk 7488b35] Revert "E" 1 file changed, 1 deletion(-) The commit message contains a nice message explaining exactly what commit you revert if any history looker decides to peruse the commits. A git log -p -1 shows the contents of the change too commit 7488b357e64cf426eaa3390a6c406e2d10f63f40 (HEAD -> trunk) Author: ThePrimeagen ThePrimeagen@netflix.com Date: Sun Feb 25 20:35:16 2024 -0700

← Previous Next → Revert "E" This reverts commit a665b08996994c2e6620a6367b0ab524be221cb2. diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change

Git reset This has such a wide range of responsibilites from get rid of what is in the worktree or the index to walking back a commit Soft Git reset soft can be very useful if you need to make a commit that is partially finished and you want to edit that commit and change the contents ➜ hello-git git:(trunk) git log -p --oneline -1 7488b35 (HEAD -> trunk) Revert "E" diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md

+++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change Now lets say we need to continue to edit our previous commit. We have two options.

  1. we could make changes and use commit --amend . If you are not familiar with git commit --amend it allows you to meld the current staged changes into the previous commit and edit the message.
  2. we could use git reset --soft HEAD~1 to move trunk back one commit and alter the index and worktree to match the contents of the commit. Note Both operations will change the sha since we are changing the graph fundamentally

Problem Use git reset --soft HEAD1 to move trunk back one commit. This should make the working state of your branch contain the changes of the revert instead of the revert commit being in the graph inspect state of your git branch via git log and git status git commit back the reverted change with a new message Solution ➜ hello-git git:(trunk) git reset --soft HEAD1

➜ hello-git git:(trunk) git log -p --oneline -1 d53a122 (HEAD -> trunk) A + 10 diff --git a/README.md b/README.md index 68dae75..eb42c13 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 7 +A + 10 D E remote-change Notice log -p shows us that the current commit (-1) is not the revert. We have successfully moved back trunk ! now check out the git status! ➜ hello-git git:(trunk) git status On branch trunk Changes to be committed: (use "git restore --staged ..." to unstage) modified: README.md Its the revert we just got done doing! ➜ hello-git git:(trunk) git diff --staged diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change

commit the soft reset contents ➜ hello-git git:(trunk) git commit -m 'its not just a revert anymore' [trunk ce41293] its not just a revert anymore 1 file changed, 1 deletion(-) ➜ hello-git git:(trunk) git log --oneline -2 ce41293 (HEAD -> trunk) its not just a revert anymore d53a122 A + 10 NOTE Obviously this is dangerous as we have just altered history of a branch The basics of soft is to reset the history to the point you want (HEAD~1) and the index and worktree will contain the changes whence you came (Our revert in this example). We could have used soft reset and went back several commits and we would have all their changes.

Hard Hard will do the same thing as soft except it will drop changes to the index and worktree. That means any work that is being tracked by git will be destroyed Problem

Before we try walking back a commit, lets first create a local change and see what happens when we execute git reset --hard Add a small change to README.md and create a new file, foo.md , with any change you see fit, then execute git reset --hard which will destroy index and worktree changes Solution ➜ hello-git git:(trunk) echo "HELLO" >> README.md ➜ hello-git git:(trunk) echo "Foo" > foo.md ➜ hello-git git:(trunk) git status On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md Untracked files: (use "git add ..." to include in what will be committed) foo.md

no changes added to commit (use "git add" and/or "git commit -a" Note • foo.md is not tracked by git • README.md is tracked and the changes are present in the worktree ➜ hello-git git:(trunk) git reset --hard HEAD is now at ce41293 its not just a revert anymore ➜ hello-git git:(trunk) git status On branch trunk Untracked files: (use "git add ..." to include in what will be committed) foo.md nothing added to commit but untracked files present (use "git add" Notice that it destroyed all work to README.md. That is because --hard will reset ALL work in the worktree and index (staging area). So even if we had git add README.md it would still have been reset back to the HEAD state. foo.md did not get destroyed because git is not tracking the file in any way.

Problem Destroy foo by using git add and git reset --hard Solution ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git reset --hard HEAD is now at ce41293 its not just a revert anymore ➜ hello-git git:(trunk) git status On branch trunk nothing to commit, working tree clean Notice that now we destroyed our work with foo.md. That is because we started to track foo.md by adding to the index (staging area). Now when we called git reset --hard it destroyed that work. NOTE CAUTION WITH RESET HARD

It can destroy your work and it can be virtually impossible to get back that work. Git reset --hard with HEAD~1 We can walk back our tree just like soft, except we discard the changes

Problem Try it now. Try walking back our commit with git reset --hard HEAD1 Solution ➜ hello-git git:(trunk) git reset --hard HEAD1 HEAD is now at d53a122 A + 10 ➜ hello-git git:(trunk) git status On branch trunk nothing to commit, working tree clean Not only did we reset trunk one step back, we also discarded all changes.

Challenge Question! Can you restore hello-git to the position it was BEFORE we started this section? Solution

➜ hello-git git:(trunk) git reflog ... 14 other entries ... # i did extra work here 7488b35 (tag: git-reset-start) HEAD@{15}: commit: Revert "E" So we can see our previous commit with Revert "E" . Lets move trunk back to there. ➜ hello-git git:(trunk) git checkout 7488b35 Note: switching to '7488b35'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to HEAD is now at 7488b35 Revert "E" ➜ hello-git git:(7488b35) git branch -D trunk Deleted branch trunk (was d53a122). ➜ hello-git git:(7488b35) git checkout -b trunk Switched to a new branch 'trunk' ➜ hello-git git:(trunk) Look at that. We have checked out that point in time, deleted our current version of trunk , and then checked out a new version of trunk.

Note There are more things you can do with git reset but i find i use restore and git reset --hard only. Git reset --hard to restore me back to HEAD cleanly and restore to remove a staged file.

EVERY←TH PIrNeGvio YuOs U'LL NEED TO KNOW ABOUT GIT NeGxITt G→UD RESET Reset – Everything You'll Need to Know About Git https://theprimeagen.github.io/fem-git/lessons/git-gud/reset

Stash is not that great Worktrees can prevent one of the most annoying problems of using git. Stashing. But wait... did we not just learn stashing? Yes. So is it bad? No. ... I am confused Worktree You are working on feature branch foo_bar . You are making great progress and you are in flow state. Just then, slack pings, lo and behold an emergency investigation is needed on

main .

  1. You can git add . any non tracked files to the index (staging area) and then stash this change then change branches to do the investigation.
  2. You can commit this change and change branches
  3. Use Worktrees What is a worktree? Generally when we say worktree we mean a linked working tree When you git init a repo it creats the main working tree . A linked working trees is a just another tree much like main working tree just without all the git history within the .git directory. In fact, the .git directory is not a directory at all, but just a file pointing to the main working tree directory.

Worktree Operations Add ➜ git worktree add The basename of the path will be used as the branch name. List ➜ git worktree list Delete There are a couple ways to go about this

  1. you can use git and git worktree remove ../foo-bar
  2. you can use rm -rf and git worktree prune Benefits

they are cheap to make since they don't need .git history. They are just a pointer. They are just slightly more expensive than a branch. But you get a commit to work on that is outside your main working tree. That means if you need to pivot quickly you don't have to commit, stash, or anything, just create a new linked working tree! Cons if each branch has a high setup cost. like if your npm install takes 100 minutes due to everything-js dependency, worktrees can be more of a pain. Problem create a worktree of hello-git with the path name ending in foo-bar . Suggestion, ../foo-bar when completed check out .git found in the linked working tree. What's different about this .git vs a main working tree .git?

Solution ➜ hello-git git:(trunk) git worktree add ../foo-bar Preparing worktree (new branch 'foo-bar') HEAD is now at 7488b35 Revert "E" you will notice that when you change into ../foo-bar that the branch name is the path's basename ➜ hello-git git:(trunk) cd ../foo-bar ➜ foo-bar git:(foo-bar) Checking out .git ➜ foo-bar git:(foo-bar) ls -la total 24 drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Feb 29 19:00 . drwxrwxr-x 128 ThePrimeagen ThePrimeagen 4096 Feb 29 19:00 .. -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 59 Feb 29 19:00 bar.md -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 65 Feb 29 19:00 .git -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 41 Feb 29 19:00 README.md

-rw-rw-r-- 1 ThePrimeagen ThePrimeagen 24 Feb 29 19:00 upstream.md .git is a file! what! ➜ foo-bar git:(foo-bar) cat .git gitdir: /home/ThePrimeagen/personal/hello-git/.git/worktrees/foo-bar .git file shows that it is just a pointer to the main working tree. That is how it knows how to find all of the information and why you can switch branches in a linked working tree. Your edits are making edits to the main working tree's .git state In other words, working trees are "light weight clones" This means you NEVER have to stash again, if you don't want to. You can also check out an existing branch with git worktree add Problem

Go back to hello-git and list out the your linked working tree Solution ➜ foo-bar git:(foo-bar) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar]

Problem Delete the working tree with rm -rf and see if you can discover through .git that your worktree no longer is active. Then delete the worktree Solution ➜ hello-git git:(trunk) rm -rf ../foo-bar ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar] prunable [foo-bar] prunable git tells you that foo-bar is prunable (its been deleted outside of git!) That means to delete, just run prune ➜ hello-git git:(trunk) git worktree prune

➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] How did git know it was prunable? ➜ hello-git git:(trunk) ls -la .git/worktrees total 12 drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 . drwxrwxr-x 9 mpaulson mpaulson 4096 Mar 20 12:27 .. drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 foo-bar ➜ hello-git git:(trunk) ls -la .git/worktrees/foo-bar total 32 drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 . drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 .. -rw-rw-r-- 1 mpaulson mpaulson 6 Mar 20 12:27 commondir -rw-rw-r-- 1 mpaulson mpaulson 37 Mar 20 12:27 gitdir -rw-rw-r-- 1 mpaulson mpaulson 24 Mar 20 12:27 HEAD -rw-rw-r-- 1 mpaulson mpaulson 289 Mar 20 12:27 index drwxrwxr-x 2 mpaulson mpaulson 4096 Mar 20 12:27 logs -rw-rw-r-- 1 mpaulson mpaulson 41 Mar 20 12:27 ORIG_HEAD the gitdir no longer exists, therefore we can prune this worktree!

Problem Recreate a worktree and delete it through .git this time. Solution ➜ hello-git git:(trunk) git worktree add ../foo-bar Preparing worktree (checking out 'foo-bar') HEAD is now at 7488b35 Revert "E" ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar] ➜ hello-git git:(trunk) git worktree remove ../foo-bar ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] ➜ hello-git git:(trunk) ls .. | grep foo-bar ➜ hello-git git:(trunk)

I love worktrees I think they are one of the best ways to use git as they allow you to keep state based on each branch rather than on directory. This allows for partial changes just to remain partial changes instead of doing the commit, stash, or rebase squash dance. I would highly recommend getting use to working with them.

← Previous Next → Other Downsides Its not all upside when it comes to git worktrees. • rust can easily eat up a couple gigs per branch • npm installing on each branch can take a minute • go is great • .env files can be a bit of a pain

Named locations within Git History At some point there are changes in which represents a version of your sofware you want named. This could be a version you are releasing as an open source library or an internal note for the public version of a website Git has you covered with tags What is a tag? The best way to think about a tag is a branch that cannot be changed. It can only be deleted

How to use tags git tag # to create git tag -d # delete git tag # to list git checkout # to checkout a tag

Problem Create a tag List out your tags Do tags show up in log ? Solution ➜ hello-git git:(trunk) git tag my-first-tag # creates a tag at the current ➜ hello-git git:(trunk) git tag # list tags my-first-tag ➜ hello-git git:(trunk) git log --oneline -3 d0ba67f (HEAD -> trunk, tag: my-first-tag) ci work d53a122 A + 10 ec6930d merged They do show up! Even with a nice tag: prefix If you check out a tag, you go into detached HEAD . This is because tags are immutable and you cannot change them, so checking them out is no different

than checking out a commit by its sha ➜ hello-git git:(trunk) git checkout my-first-tag Note: switching to 'my-first-tag'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to HEAD is now at d0ba67f ci work ➜ hello-git git:(d0ba67f)

Git push You can push tags to a remote via git push --tags and pull via git pull --tags Problem Pull your tag to remote-git

Solution the --tags will fetch branches as well as tags ➜ remote-git git:(foo-bar-baz) git pull --tags remote: Enumerating objects: 15, done. remote: Counting objects: 100% (15/15), done. remote: Compressing objects: 100% (7/7), done. remote: Total 11 (delta 2), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (11/11), 951 bytes | 951.00 KiB/s, From ../hello-git

[new branch] baz -> origin/baz

[new branch] foo-bar -> origin/foo-bar

d53a122..d0ba67f trunk -> origin/trunk

  • 4f452da...e6d9d4b trunk2 -> origin/trunk2 (forced update)

[new tag] my-first-tag -> my-first-tag

Already up to date. ➜ remote-git git:(foo-bar-baz)

← Previous Next →

The tools I use Thanks t-pope for this lovely plugin • add and inspect change • commit • diff and diff view • push • conflict • stash • rebase

← Previous Next →

← Previous Why did I not cover git diff I think that is best done with a program. Command line diffs are just hard to check out.

Git Branching You don't always want to be developing on the main branch. Sometimes you need a feature that is developed off the main line, such that you can return to the main line, update the code, branch off, and perform some immediate needed fix. This is where git branches come in. They are cheap, virtually free, to create. With our of understanding git internals this will become much more clear throughout this section Problem

Since we are in a new git repo, lets create our initial commit. We will be using the hello-git you just created previously to test the trunk default branch setting I would like a README.md with one line, A , and a commit message of A Solution ➜ hello-git git:(trunk) echo "A" > README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A' [trunk (root-commit) cb75afe] A 1 file changed, 1 insertion(+) create mode 100644 README.md

Creating branches Creating branches is easy git branch foo Will create a new branch named foo

Problem Try it out, create a branch named foo Solution ➜ hello-git git:(trunk) git branch foo ➜ hello-git git:(trunk)

Well... what happened? • A branch was created pointing to the same commit as trunk • We remain on trunk List out branches We can observe this by executing git branch . git branch will display all of our local branches git branch

Problem List out your branches. How do you know what branch you are on? Use git log with --decorate to see where the branches point at and prove that foo points to the same commit as trunk Side Note If you use git log and you see the same output as when you use --decorate this happens. There are some internal settings that automagically make this happen git log > out cat out you may notice that you lose the branching information that you had.

Solution git branch foo

trunk

(END) That shows we have 2 branches, one foo and one trunk . trunk is selected, and you can tell because of the * lets execute git log and write down our sha for this specific commit commit cb75afebfac407bfc860dd854b626322a6dc8345 (HEAD -> trunk, foo) Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:04:01 2024 -0700 You will notice that the commit has HEAD -> trunk, foo . foo is pointing to this commit. A branch POINTS to a commit, it is not a set of changes.

A Touch of Git Foo Can you find your branch details in .git ? Ahh yes... its right there... ➜ hello-git git:(trunk) cat .git/refs/heads/foo cb75afebfac407bfc860dd854b626322a6dc8345 ➜ hello-git git:(trunk) cat .git/refs/heads/trunk cb75afebfac407bfc860dd854b626322a6dc8345

Switching branches You can switch branches easily in two ways git switch git checkout checkout is a more versatile operation and i am personally just in the habbit of using it. You can use whatever you want / are comfortable with Problem

switch to branch foo Solution ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo'

Problem Now that you are foo lets create another commit with message B and a new line added to second.md of "B" Then do it again, except replace B with C When finished use git log with what options you want, to see if you can see some differences now! This should be the setup you should have at the end of this B - C foo / A trunk

Solution ➜ hello-git git:(foo) echo "B" > second.md ➜ hello-git git:(foo) git add . ➜ hello-git git:(foo) git commit -m 'B' [foo 4ad6ccf] B 1 file changed, 1 insertion(+) create mode 100644 second.md ➜ hello-git git:(foo) echo "C" > second.md ➜ hello-git git:(foo) git add . ➜ hello-git git:(foo) git commit -m 'C' [foo 16984cb] C 1 file changed, 1 insertion(+), 1 deletion(-) Now lets execute git log and see if we can see a difference commit 16984cb1725b0bfeff43f60f92b3467ee6d525e3 (HEAD -> foo) Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:17:11 2024 -0700 C commit 4ad6ccf530547ce89cfd90cd7a6e747f9531d917 Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:15:38 2024 -0700 B commit cb75afebfac407bfc860dd854b626322a6dc8345 (trunk) Author: ThePrimeagen the.primeagen@gmail.com

Date: Sun Jan 28 10:04:01 2024 -0700 A 2 things:

  1. Notice that HEAD -> foo , or current location of git points to foo, which is 16984cb
  2. trunk is still at A . Trunk has not been updated with this code Fun side-note • try out --graph if you didn't! • try out --oneline if you haven't • try all three options together! You can delete branches If you create a branch and you wish to delete it you can use -d and -D . For more information read up in the git manual, man git-branch

Commits locked in... Ok, now that we have a branch full of changes... what do we do with it?

← Previous Next →

Prereq: You have to have the previous section finished and have the same state B --- C foo / A trunk Problem Lets start off by creating some more commits and adding them to trunk . This way our branches diverge. Meaning they have commits that are unique in both and have a common ancestor, A .

Please add D and E in the same way we added B and C to trunk Desired State: B --- C foo / A --- D --- E trunk Solution ➜ hello-git git:(foo) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) echo "D" >> README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'D' [trunk 79c5076] D 1 file changed, 1 insertion(+)

➜ hello-git git:(trunk) echo "E" >> README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'E' [trunk a665b08] E 1 file changed, 1 insertion(+) Now we have the following setup B --- C foo / A --- D --- E trunk and our logs have commit messages that should make it easy to track. So now we can talk about git merge and rebase Git Merge and Rebase Remember A commit is a point in time with a specific state of the entire code base.

We have work done, but its on another branch. We need to get it back into our main branch, trunk ... but how? There really is only one strategy to merge code from one branch to another, but depending on the state different outcomes can happen. rebase can help put a branch into a pristine state What is a merge? A merge is attempting to combine two histories together that have diverged at some point in the past. There is a common commit point between the two, this is referred to as the best common ancestor ("merge base" is often the term used in the docs).

To put it simply, the first in common parent is the best common ancestor. git then merges the sets of commits onto the merge base and creates a new commit at the tip of the branch that is being merged on with all the changes combined into one commit. there is also extra information in the logs about these types of commits How to merge Merging is quite simple. The branch your on is the target branch and the branch you name in will be the source branch.

git merge Problem Given the following state, merge foo onto trunk . Since we want to keep foo and trunk in its current state for other problems later in this lesson. Please start by branching off of trunk . I named mine trunk-merge-foo Finally, when you are done use git log to see the resulting state of trunk-merge-foo State of your git B --- C foo

/ A --- D --- E trunk-merge-foo Solution ➜ hello-git git:(trunk) git checkout -b trunk-merge-foo Switched to a new branch 'trunk-merge-foo' git checkout -b trunk-merge-foo is the same thing as the following: git branch trunk-merge-foo git checkout trunk-merge-foo

Fun fact From man git-switch you can see the following: git switch [] (-c|-C) [] Meaning we could of created a new branch by using git switch -c trunk-merge-foo and switched to it. lets merge in foo into trunk-merge-foo ➜ hello-git git:(trunk-merge-foo) git merge foo At this point you are presented with the EDITOR . Git will use your system's default editor for you to accept/make any changes to commit messages. You are presented with the message that will be in the logs after merging these two branches together. I typically do not edit this merge message Merge branch 'foo' into trunk-merge-foo

  1. Please enter a commit message to explain why this merge is necessary,
  2. especially if it merges an updated upstream into a topic branch.

  1. Lines starting with '#' will be ignored, and an empty message aborts
  2. the commit. If vim is your default git editor :wq to save the message and confirm the commit ➜ hello-git git:(trunk-merge-foo) git merge foo Merge made by the 'ort' strategy. second.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 second.md Lets use log to check out what has happened here • --parents adds 1 to two extra shas signifying the parent chain. This is duplicated by --graph but instead of a graphical representation, its with shas. ➜ hello-git git:(trunk-merge-foo) git log --oneline --decorate --graph * ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch |
    | * 16984cb 4ad6ccf (foo) C | * 4ad6ccf cb75afe B

| a665b08 79c5076 (trunk) E

| 79c5076 cb75afe D

|/

cb75afe A

How to read the lines | * 16984cb 4ad6ccf (foo) C commit 16984cb has a parent of 4ad6ccf with a named branch of foo with a commit message of C The only commit that has a different look is the first line of the logs, which is the latest commit.

ccf9a73 a665b08 16984cb (HEAD -> trunk-merge-foo) Merge branch

ccf9a73 has two parents, a665b08 and 16984cb . If you look at those commits, they are the commits of trunk and foo . git merge merged those two commits together by finding the best common ancestor (merge

base cb75afe ) and playing the commits one at a time, start at cb75afe , creating a new commit, a merge commit, ccf9a73 . Problem Create the following git setup: X - Y bar / A --- D --- E trunk That means: • branch bar off trunk • add two commits with message X then Y • create your changes, X and Y , in bar.md

Solution First checkout trunk then use trunk to create branch bar ➜ hello-git git:(trunk-merge-foo) git checkout trunk ➜ hello-git git:(trunk) git checkout -b bar Create the two commits, X and Y , on bar ➜ hello-git git:(bar) echo "X" > bar.md ➜ hello-git git:(bar) git add bar.md ➜ hello-git git:(bar) git commit -m "X" [bar 2f43452] X 1 file changed, 1 insertion(+) create mode 100644 bar.md ➜ hello-git git:(bar) echo "Y" >> bar.md ➜ hello-git git:(bar) git add bar.md

➜ hello-git git:(bar) git commit -m "Y" [bar b23e632] Y 1 file changed, 1 insertion(+) we can verify that we have similar histories by using git log again. ➜ hello-git git:(bar) git log --oneline --decorate --graph

  1. You should see the same ordering

b23e632 (HEAD -> bar) Y

2f43452 X

a665b08 (trunk) E

79c5076 D

cb75afe A

Problem

merge bar onto trunk . Do not create a separate branch this time. Once you merge see if you can spot the difference between a "3 way merge" vs a "fast forward" merge. Solution First we simply checkout trunk then execute git merge bar ➜ hello-git git:(bar) git checkout trunk M README.md Switched to branch 'trunk' ➜ hello-git git:(trunk) git merge bar Updating a665b08..b23e632 Fast-forward bar.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 bar.md

First difference In the merge notes we see the following line: Fast-forward That means we have a fast forward merge Second, the logs You can view this FF merge with git log ➜ hello-git git:(trunk) git log --oneline --decorate --graph

b23e632 (HEAD -> trunk, bar) Y

2f43452 X

a665b08 E

79c5076 D

cb75afe A

Excalidraw what happens

Rebase Rebasing often gets a bad wrap. Part of this is because people don't really know why or when to use rebase and will end up using it incorrectly and thus yelling on the twitters that it "ruins their entire life." Gotta love twitter. With the following repo setup B --- C foo / A --- D --- E --- X --- Y trunk We can demonstrate the power of rebase. What rebase will do is update foo to point to Y instead of A .

B --- C foo / A --- D --- E --- X --- Y trunk THAT IS IT That is all rebase does here. It updates the commit where the branch originally points to This also means that in we decide to merge foo into trunk we can do a ffmerge! What Rebase Does

The basic steps of rebase is the following:

  1. execute git rebase . I will refer to the current branch as later on
  2. checkout the latest commit on
  3. play one commit at a time from
  4. once finished will update to the current commit sha How rebase works is important, it will lead to complication down the road if you don't understand this key fact We will address that later Problem

rebase foo with trunk . Please create a separate branch foo-rebasetrunk . This branch should be created off of foo . Once you have rebased foo-rebase-trunk with trunk check out the git logs with --graph and --decorate to see what has happened ( --oneline can make it easier to read) Solution First we will create a new branch off of foo and call it foo-rebase-trunk ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo' ➜ hello-git git:(foo) git checkout -b foo-rebase-trunk Switched to a new branch 'foo-rebase-trunk' Now we need to perform the rebase

➜ hello-git git:(foo-rebase-trunk) git rebase trunk Successfully rebased and updated refs/heads/foo-rebase-trunk. git log time to view what has happenend ➜ hello-git git:(foo-rebase-trunk) git log --oneline --decorate --graph

  1. you should have same history

3ade655 (HEAD -> foo-rebase-trunk) C

d810248 B

b23e632 (trunk, bar) Y

2f43452 X

a665b08 E

79c5076 D

cb75afe A

You will see that trunk (which contains bar after our fast forward merge) is now the new base for foo-rebase-trunk . In other words, foo is no longer diverging from trunk . If we choose to merge foo into trunk we can do so via ff-merge (no merge commit).

Some pros When using rebase you can have a clean history with no merge commits. If you are someone who uses git log a lot this can really help with searching. Some cons It alters history of a branch. That means that if you already had foo on a remote git, you would have to force push it to the remote again. We will go over this more it shortly Some cautionary words NEVER CHANGE HISTORY OF A PUBLIC BRANCH. In other words, don't ever change history of trunk . But your own personal branch? I don't think it matters and i think having a nice clean history can be very beneficial if you use git to search for changes through time.

Content Licensed Under CC-BY-NC-4.0 Code Samples and Excercises Licensed Under Apache 2.0 ← Previous Next → Wait.. isn't there more? Yes, there is a lot more, but we are not going to go over it without more foundational knowledge

Site Designed by Alex Danielson

HEAD You have seen it a bunch of times and probably can guess what it means... Throughout this course you may have notice that whenever we call git log there a HEAD within the logs. What is HEAD ? Lets do some basic experimenting first ➜ hello-git git:(foo-rebase-trunk) git checkout trunk

Switched to branch 'trunk' ➜ hello-git git:(trunk) git log --oneline --decorate --graph

b23e632 (HEAD -> trunk, bar) Y

... ^--- we currently have trunk checked out swapping branches and doing another git log will result in similar results. ➜ hello-git git:(trunk) git checkout foo Switched to branch 'foo' ➜ hello-git git:(trunk) git log --oneline --decorate --graph

16984cb (HEAD -> foo) C

... ^--- we currently have foo checked out Problem In the .git folder can you tell what HEAD is pointing to?

Solution ➜ hello-git git:(foo) cat .git/HEAD ref: refs/heads/foo That means as foo updates HEAD does not have to! Reflog

Its not just a wizards tool Introducing git reflog . The default command of git reflog allows you to see where head has been. Thought Exercise Give reflog command a try. What do you see. Think about what you have had checked out.

Answers ➜ hello-git git:(trunk) git reflog

  1. You should see something similar if you have been following along 16984cb (HEAD -> foo) HEAD@{0}: checkout: moving from trunk to foo b23e632 (trunk, bar) HEAD@{1}: checkout: moving from foo-rebase-trunk to trunk 3ade655 (foo-rebase-trunk) HEAD@{2}: rebase (finish): returning to refs/heads/foo-3ade655 (foo-rebase-trunk) HEAD@{3}: rebase (pick): C d810248 HEAD@{4}: rebase (pick): B ... If you look carefully you will see every checkout you have done of any branch is listed, in time based ordering Search Can you find where this information is stored inside .git

Solution In reflog command ➜ hello-git git:(foo) git reflog -3 44016b0 (HEAD -> foo) HEAD@{0}: checkout: moving from foo-rebase-trunk to foo 11a3f97 (foo-rebase-trunk) HEAD@{1}: rebase (finish): returning to refs/heads/foo-11a3f97 (foo-rebase-trunk) HEAD@{2}: rebase (pick): C In .git/logs/HEAD ➜ hello-git git:(foo) cat .git/logs/HEAD | tail -3 6fd6e31690d138de9380544d01842841b0d37963 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 11a3f97202b9f28f5511dbfaa823168756cca03e 44016b016fd77883d98b1fce4c3dea2cf877f408 Again. Its not magic

Problem This will be an interesting set of tasks to do, but can be quite useful in understanding reflog.

  1. create a new branch off of trunk, call it baz .
  2. add one commit to baz . Do it in a new file baz.md
  3. switch back to trunk and delete baz ( git branch -D baz from earlier)
  4. can you bring back from the dead the commit sha of baz ?

Solution First thing we will do is create the baz branch then add a commit to it ➜ hello-git git:(foo) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) git checkout -b baz Switched to a new branch 'baz' ➜ hello-git git:(baz) echo "baz" > baz.md ➜ hello-git git:(baz) git add baz.md ➜ hello-git git:(baz) git commit -m 'Baz' [baz f330d23] Baz 1 file changed, 1 insertion(+) create mode 100644 baz.md Next, lets switch back to trunk and delete the branch baz ➜ hello-git git:(baz) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) git branch -D baz Deleted branch baz (was f330d23). Now we recover the branch via reflog (should be obvious because i just talked about it) ➜ hello-git git:(baz) git reflog

b23e632 (HEAD -> trunk, bar) HEAD@{0}: checkout: moving from baz to trunk f330d23 HEAD@{1}: commit: Baz # <--- there is our target commit b23e632 (HEAD -> trunk, bar) HEAD@{2}: checkout: moving from trunk to baz ... Now that we see f330d23 is our target commit, what can we do to recover the work? There are a lot of possibilities. Problem Use our knowledge of how git plumbing works to retrieve the contents of a commit, use this super power to grab out the file baz.md

Solution To get the information we need we will have to use the commit to get the tree sha and the tree sha for the blob, file baz.md , contents

  1. Step 1, get the file tree try cat-file'ing the commit sha ➜ hello-git git:(trunk) git cat-file -p f330d23 tree d2d8e10a88b4e985003930d45c5c488abe712e6b # <-- the tree parent b23e6320e6fba64d93338543dcbcdcc9caadb71e author ThePrimeagen ThePrimeagen@netflix.com 1707069560 -0700 committer ThePrimeagen ThePrimeagen@netflix.com 1707069560 -0700 Baz
  2. Step 2, print out the trees content. baz.md is our taget file ➜ hello-git git:(trunk) git cat-file -p d2d8e10 100644 blob d3045e2c2d21fcf774700b3d5fa681cf26b300ad README.md 100644 blob 16858db7afb62f3e027d8f9379085d3567bcac62 bar.md 100644 blob 76018072e09c5d31c8c6e3113b8aa0fe625195ca baz.md # <-- target file
  3. Step 3, print the file and copy the contents! ➜ hello-git git:(trunk) git cat-file -p 7601807 > baz.md Pure wizardry

Problem Lets not use the internals, what is another way, using the commands we have gone over thus far to get the same information. We flexed... but we didn't have to. State the change, don't perform it Solution Remember a branch is just a pointer to a commit ➜ hello-git git:(trunk) git merge f330d23

Updating b23e632..f330d23 Fast-forward baz.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 baz.md I can just merge that change directly into trunk! Warning If histories have diverged far enough, this could cause some problems as you wouldn't just be merging the one commit, but all the commits in betwixt f330d23 and trunk Cherry Pick from man git-cherry-pick Given one or more existing commits, apply the change each one introduces, recording a new commit for each. This requires your working tree to be clean (no modifications from the HEAD commit).

Problem Git cherry-pick a try and see if you can also get the changes from baz into trunk Solution

Content Licensed Under CC-BY-NC-4.0 Code Samples and Excercises Licensed Under Apache 2.0 Site Designed ← Previous by Alex Danielson Next → ➜ hello-git git:(trunk) git cherry-pick f330d23 [trunk e6d9d4b] Baz Date: Sun Feb 4 10:59:20 2024 -0700 1 file changed, 1 insertion(+) create mode 100644 baz.md I have used cherry pick a ton of times throughout my career as a software dev Draw excalidraw the downsides of merge vs cherry-pick

Small note I will often say the phrase upstream to refer to hello-git . This is because i am treating hello-git similar to github . A Problem We All Face You have made a change... but you need to pull in changes from a remote repo, what do you do? commit changes then rebase or merge? (i would pick rebase)

The problems of merge and #1 If you create a commit with changes that are half baked just to pull in origin changes you will have your (potentially) broken commit, a merge commit, then your fixing commit. If you need to revert these changes, it will get more difficult (more on reverting later). This is a good case for rebase Your changes with rebase, will get put on top of all the incoming changes which allows your partial commit easier to work with. This style also makes squashing easier But i would rather have my partial commit not committed yet This is where stash comes in.

Worktrees Often the superior approach and we will discuss them later Stash git stash will take every change tracked by git (change to index + change to work tree) and store that result, much like a commit, into the "stash." To quote from the man page Use git stash when you want to record the current state of the working directory and the index, but want to go back to a clean working directory. The command saves your local modifications away and reverts the working directory to match the HEAD commit.

Stash is a STACK of temporary changes Operations You can push your changes into the stack by using git stash Stashes, much like commits, can come with a message ( -m "" ) git stash -m "my lovely message here"

Stashes can be listed out: git stash list git stash show [--index ] To pop the latest stash: git stash pop To pop a stash at an index: git stash pop --index # works well with git stash list Remember man git-stash is your friend. if you forget how a command works, please review the manual first! Its the authority of how to use the tool

Problem use branch trunk

  1. create an upstream change. Commit a small change to hello-git echo "upstream change" >> upstream.md
  2. add a small change to remote-git don't commit echo "downstream change" >> README.md
  3. now that we have an active tracked change pull in the upstream change What error do you get?

Solution

  1. create the origin changes

  2. make sure you are on branch trunk cd /path/to/hello-git echo "upstream change" >> upstream.md git add upstream.md git commit -m "upstream changes" [trunk 6849c67] upstream changes 1 file changed, 1 insertion(+) create mode 100644 upstream.md

  3. create the downstream changes but do not commit

  4. make sure you are on branch trunk cd /path/to/remote-git echo "downstream change" >> README.md

  5. validate that the changes are tracked by the worktree ➜ remote-git git:(trunk) git status On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

  6. Try to pull in origin changes ➜ remote-git git:(trunk) git pull error: cannot pull with rebase: You have unstaged changes. error: please commit or stash them. Error error: cannot pull with rebase: You have unstaged changes. error: please commit or stash them.

Problem Now that we have produced the error and the answer is clear: use stash. Lets play around with stash a bit more. To become more familiar perform the following:

  1. stash your changes
  2. view your stash list
  3. pop your stashed chages
  4. stash your changes but with a custom message
  5. create more changes and stash those so we have 2 in the list
  6. pull in the upstream's changes

Solution This will be a long set of changes, but they are all pretty simple.

  1. stash your changes ➜ remote-git git:(trunk) git stash Saved working directory and index state WIP on trunk: 42afc8d A remote ➜ remote-git git:(trunk) git status On branch trunk nothing to commit, working tree clean NOTE If you have changes to non indexed files then they will not be added to the stash command. Careful not to lose them.
  2. view your stash list ➜ remote-git git:(trunk) git stash list stash@{0}: WIP on trunk: 42afc8d A remote change (END)

To view the current stashed change (the one in position 0) use show diff --git a/README.md b/README.md index 9f276a6..2ca5a19 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ A D E remote-change +downstream change 3. pop your stashed chages ➜ remote-git git:(trunk) git stash pop On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Dropped refs/stash@{0} (e318e20fb946c2a78700611129d9ae040b4cc80c) 4. stash your changes but with a custom message ➜ remote-git git:(trunk) git stash -m "my very nice change about..." Saved working directory and index state On trunk: my very nice ➜ remote-git git:(trunk) git stash list stash@{0}: On trunk: my very nice change about...

  1. create more changes and stash those so we have 2 in the list Note Having named stashes can be useful if you come back a week later to a project and forgot what you have stashed / where you have put your changes ➜ remote-git git:(trunk) echo "some other change" >> README.md ➜ remote-git git:(trunk) git stash -m "other changes" Saved working directory and index state On trunk: other changes ➜ remote-git git:(trunk) git stash list stash@{0}: On trunk: other changes stash@{1}: On trunk: my very nice change about...
  2. pull in the upstream's changes ➜ remote-git git:(trunk) git pull Updating 42afc8d..6849c67 Fast-forward upstream.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 upstream.md

Problem Now we need to get back our original changes. Please pop the changes of our first stash. Remember the stash is a stack like data structure. Therefore your first change isn't the first item in the stash. Solution We need to use pop and --index to accomplish this.

➜ remote-git git:(trunk) git stash pop --index 1 On branch trunk Your branch is up to date with 'origin/trunk'. Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Dropped refs/stash@{1} (c58f8ce5300e4207031438236c5732901666a43a) ➜ remote-git git:(trunk) git commit -m 'greatest changes' [trunk 7282922] greatest changes 1 file changed, 1 insertion(+) Stashing is quite powerful and allows you to be able to bring in upstream changes without losing your work or creating commits which can be annoying to deal with! Worktrees

← Previous Next → We will talk more about these, but generally this is my favorite way to work in a fast EVERYTHI NevGol vYinOg Uco'dLeLb aNseEED TO KNOW ABOUT

MOAR Rebasing We have briefly talked about rebasing as being able to realign where the branch point exists for one branch onto another. In other words, you make history linear. Here is a simple visual example E - F - G topic / A - B - C - D master ➜ some-git git:(topic) git rebase master # we are on branch topic Assuming everything went off without a hitch, you will have the following state E - F - G topic

/ A - B - C - D master Interactive Rebasing and Squashing You may find yourself on a team that asks you to "squash" your commits. What is meant by this is interactive rebase squash. In other words: the aforementioned diagram we can transform from E - F - G topic / A - B - C - D master

To

  1. notice this is one commit EFG topic / A - B - C - D master Along with squashing, interactive rebasing allows you to edit messages and more Lets create a situation where we can interactively squash our commits and provide some proper messaging! But to get there we need to cover a LOT of ground

For us to cover this... we have to talk about the most dreaded topic in git Conflicts I hate them You hate them but its good to know how to resolve them. PLEASE A note for all who have a nice git plugin to make this process easier. Please do not use any fancy tools, lets just manually resolve these.

To create a conflict The easiest way to create a conflict is when you have two changes to a repo that cannot be resolved by the merging strategies. In other words, edit the same line.

Problem Create a conflict with remote-git and hello-git . To do this, please create a commit in both hello-git and remote-git editing the same location within a file. To accomplish this

  1. Use trunk in both repos
  2. change hello-git 's README.md first line to A + 1 and commit
  3. change remote-git 's README.md first line to A + 2 and commit
  4. pull hello-git into remote-git to create the conflict Solution
  5. Changed hello-git trunk 's README.md's first line cd path/to/hello-git ➜ hello-git git:(trunk) vim README.md

A + 1 D E remote-change Commit the change ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 1' [trunk 9648be0] A + 1 1 file changed, 1 insertion(+), 1 deletion(-) 2. Changed remote-git trunk 's README.md's first line cd path/to/remote-git ➜ remote-git git:(trunk) vim README.md A + 2 D E remote-change Commit the change

➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 2' [trunk 6eb0a42] A + 2 1 file changed, 1 insertion(+), 1 deletion(-) Time to pull down change from remote ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 303 bytes | 303.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 8058c68...9648be0 trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md Automatic merge failed; fix conflicts and then commit the result. We are now officially conflicted!

What is in conflict? Often its not obvious what is in conflict just by the message (if there is a large set of changes). So a simple way to see what is conflicted is by checking out the status ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" Note There are some things to take from this status message

  1. Unmerged path's contains README.md and it says both modified . That is your key to what needs to be resolved
  2. You can abort the merge due to the conflict by executing git merge --abort Resolving a Conflict When you cat out the file that is conflicted, README.md , you will see some additional information in the file that was not there before ➜ remote-git git:(trunk) vim README.md
  3. This is the look of vim

1 <<<<<<< HEAD 1 A + 2 2 ======= 3 A + 1 4 >>>>>>> 9648be0ae764528ac63759d7e49fc623ae0af373 5 D 6 E 7 remote-change 8 downstream change So some important information is present.

  1. Any >>>>, ======, <<<<< denote parts of the conflict. 1 <<<<<<< HEAD This stats that HEAD s conflicted change starts here and continues until the ======= line. You can confirm this with git log -p -1 You can verify this by noticing that the change in the HEAD section is A + 2 which is the change that is in the remote-git trunk branch and is the HEAD location of remote-git

======= denotes the separation of the two merges The end of the merge conflict is denoted with >>>> and sha of the incoming conflicted change

9648be0ae764528ac63759d7e49fc623ae0af373 Problem Validate that the sha, mine is 9648be0ae764528ac63759d7e49fc623ae0af373, belongs to hello-git

Solution You can validate the bottom sha belonging to hello-git by using the following log ➜ hello-git git:(trunk) git log --oneline -1 9648be0 (HEAD -> trunk) A + 1 -1 with git log says only show 1 commit. -3 would show 3 commits of history. Notice that the hash provided in the conflict is HEAD in hello-git and it also matches the change of A + 1

Problem We are conflicted and we need to resolve this. Use the status message to identify which file to edit and what to do after you edit the file. Lets choose a side to keep as part of the merge. We will choose my remotegit change. To choose that commit, delete line <<<<<<< HEAD and delete from ======= up to and including >>>>>>> 9648be0ae764528ac63759d7e49fc623ae0af373 In other words we are keeping the HEAD changes and dropping the 9648be0 changes (for the sake of the course you should choose the same side) After conflict has been resolved (by removing the conflict markers and the code from hello-git ) commit the merge.

Before you commit the merge check the status Solution The desired code state should be: A + 2 D E remote-change downstream change We removed all the < , = , and > lines (conflict markers) and A + 1

(change from hello-git ) Side Note Now there is technically nothing preventing you from choosing both sides, and if you did that your code would look like A + 1 A + 2 D E remote-change downstream change git status tells us the next step ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

We need to run git commit ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git status On branch trunk All conflicts fixed but you are still merging. (use "git commit" to conclude merge) ➜ remote-git git:(trunk) git commit [trunk d8a2f95] Merge branch 'trunk' of ../hello-git into trunk

  1. You will be presented with this commit Merge branch 'trunk' of ../hello-git into trunk
  2. Conflicts:
  3. README.md

  1. It looks like you may be committing a merge.
  2. If this is not correct, please run
  3. git update-ref -d MERGE_HEAD
  4. and try again.
  5. Please enter the commit message for your changes. Lines starting
  6. with '#' will be ignored, and an empty message aborts the commit.

  1. On branch trunk

  2. All conflicts fixed but you are still merging.

Thought Exercise There was no status... why?

Solution We didn't accept the changes from hello-git . We effectively deleted all the changes from remote so the change was empty. There is still a merge commit that is needed. You can validate the merge commit by a quick look at the logs d8a2f95 (HEAD -> trunk) Merge branch 'trunk' of ../hello-git into trunk 9648be0 (origin/trunk) A + 1 6eb0a42 A + 2 7282922 greatest changes 6849c67 upstream changes 42afc8d A remote change b23e632 Y 2f43452 X a665b08 E 79c5076 D cb75afe A Notice that we have a merge commit and we also have A + 1 commit. The history is not lost, but the changes are not present Try looking at the log with --graph

Problem Two conflicts are better than one, right? .... right? Ok, i agree. Lets not conflict again. Instead

  1. create a change in bar.md in hello-git ➜ hello-git git:(trunk) echo "no conflict" >> bar.md
  2. pull in change in remote

Solution Make the change ➜ hello-git git:(trunk) echo "no conflict" >> bar.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'no conflict' [trunk a9cb358] no conflict 1 file changed, 1 insertion(+) Pull in the change to remote-git ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done.

remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 297 bytes | 27.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 9ee733d...a9cb358 trunk -> origin/trunk (forced update) Merge made by the 'ort' strategy. bar.md | 1 + 1 file changed, 1 insertion(+) 1 Merge branch 'trunk' of ../hello-git into trunk 1 # Please enter a commit message to explain why this merge is necessary

2 # especially if it merges an updated upstream into a topic branch. 3 # 4 # Lines starting with '#' will be ignored, and an empty message aborts 5 # the commit. Observation Notice you do have to have another merge commit because the origin does not contain your commits, so every merge will cause a merge commit. That means you could get a pretty hairy set of commits. Check out the graph again with log --graph

Takeaway? • once you resolve a conflict and you don't take upstream's you will get merge commits until you sync your changes back to the remote Conflicts, but with rebase aren't you excited for more conflicts?

Problem What is unique about rebase that would make conflicts harder? Solution

Recall that rebase will replay all your commits after moving forward the history, which means what if a conflict happened in the past? Lets say you have the following setup: E - F - G topic / A - B - C - D master And lets pretend that C contains a change that creates a conflict with G . We rebase and we resolve the conflict and now our graph looks like the following: E - F - G topic / A - B - C - D master Then master gets another commit, Y

E - F - G topic / A - B - C - D - Y master Now if we rebase again, we will play E , F , and G . Since we are computer scientists, aka masochists, Lets do this to ourselves! Problem To ensure everything continues on going smooth, lets update trunk in hello-git with push from hello-git We don't want more merge commits...

Solution ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 15, done. Counting objects: 100% (14/14), done. Delta compression using up to 12 threads Compressing objects: 100% (7/7), done. Writing objects: 100% (9/9), 1.07 KiB | 1.07 MiB/s, done. Total 9 (delta 1), reused 0 (delta 0), pack-reused 0 remote: error: refusing to update checked out branch: refs/heads/trunk remote: error: By default, updating the current branch in a non-bare reposit remote: is denied, because it will make the index and work tree inconsistent remote: with what you pushed, and will require 'git reset --hard' remote: the work tree to HEAD. remote: remote: You can set the 'receive.denyCurrentBranch' configuration variable remote: to 'ignore' or 'warn' in the remote repository to allow pushing remote: its current branch; however, this is not recommended unless you remote: arranged to update its work tree to match what you pushed remote: other way. remote:

remote: To squelch this message and still keep the default behaviour, remote: 'receive.denyCurrentBranch' configuration variable to To ../hello-git ! [remote rejected] trunk -> trunk (branch is currently checked out) error: failed to push some refs to '../hello-git' What happened here? Observation ... ! [remote rejected] trunk -> trunk (branch is currently checked out) We cannot push to a branch that is the current branch of the target repo. This makes sense as it would cause your current branch to change out of underneath the repo that is currently being used, and if there are pending changes it could cause further havoc.

What do we do? Change branches! ➜ hello-git git:(trunk) git checkout bar Switched to branch 'bar' Now lets try again ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 15, done. Counting objects: 100% (14/14), done. Delta compression using up to 12 threads Compressing objects: 100% (7/7), done.

Writing objects: 100% (9/9), 1.07 KiB | 1.07 MiB/s, done. Total 9 (delta 1), reused 0 (delta 0), pack-reused 0 To ../hello-git a9cb358..b51e34a trunk -> trunk Problem Lets create another conflict but resolve this via rebase instead of merge .

  1. create change in hello-git and A + 2 -> A + 3 .
  2. Create another change in bar.md LAST LINE in hello-git
  3. create change in remote-git and A + 2 -> A + 4
  4. Create another change in bar.md FIRST LINE in remote-git
  5. rebase remote-git 's trunk with hello-git 's and create the conflict

Solution happy-git ➜ hello-git git:(bar) git checkout trunk Switched to branch 'trunk' ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) vim bar.md ➜ hello-git git:(trunk) git diff diff --git a/README.md b/README.md index e42f7f7..43b4231 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 2 +A + 3 D E remote-change diff --git a/bar.md b/bar.md index 04fca9f..894d904 100644 --- a/bar.md +++ b/bar.md @@ -1,3 +1,4 @@ X

Y no conflict +adding a line to the end ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 3' [trunk 958f33f] A + 3 2 files changed, 2 insertions(+), 1 deletion(-) remote-git ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) vim bar.md ➜ remote-git git:(trunk) git diff diff --git a/README.md b/README.md index e42f7f7..76c0a5e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 2 +A + 4 D E remote-change diff --git a/bar.md b/bar.md index 04fca9f..3dc9259 100644 --- a/bar.md +++ b/bar.md @@ -1,3 +1,4 @@ +first line change X Y no conflict ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 4' [trunk a1cbe6c] A + 4 2 files changed, 2 insertions(+), 1 deletion(-) Now that we are all primed and ready for a conflict.

➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 7, done. remote: Counting objects: 100% (7/7), done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (4/4), 367 bytes | 367.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 75e4992...958f33f trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md Auto-merging bar.md error: could not apply a1cbe6c... A + 4 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply a1cbe6c... A + 4 NOTICE If you read carefully you will see that bar was able to be Auto-merged

where as README was not able to be merged NOTICE We can git rebase --abort due to the conflict (much like the git merge --abort ). NOTICE This is important. Once you have resolved the conflict we need to git rebase --continue instead of git commit . GIT FU • If you did use git reset --soft HEAD~1 and then git rebase --continue

Checking out the conflict ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md You will notice that bar.md is marked (green if you have coloring) committed while README.md is unmerged. Lets fix our conflict in README.md Opening up README.md shows us the following

Thought Exercise Explain the conflict and why its different than merge A + 3 D E remote-change downstream change Answer

This was pretty tricky question. noticed that A + 3, from hello-git now takes the top spot and A + 4 takes the bottom. What are the steps of rebase? Problem Choose our conflict, hello-git 's change. (A + 3) Remember do not commit. We git rebase --continue . This signals to the rebase command that we are ready for the next commit to be played on top.

Solution ➜ remote-git git:(958f33f) git add . ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (all conflicts fixed: run "git rebase --continue") Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md ➜ remote-git git:(958f33f) git rebase --continue [detached HEAD c7b9731] A + 4 1 file changed, 1 insertion(+) Successfully rebased and updated refs/heads/trunk. A + 4

  1. Please enter the commit message for your changes. Lines starting
  2. with '#' will be ignored, and an empty message aborts the commit.

  1. interactive rebase in progress; onto 958f33f

  2. Last command done (1 command done):

  3. pick a1cbe6c A + 4

  4. No commands remaining.

  5. You are currently rebasing branch 'trunk' on '958f33f'.

  1. Changes to be committed:
  2. modified: bar.md

Since there are no more commits that cause conflicts the rebase is complete. Lets take a quick look at our logs Exercise check out the history

Result ➜ remote-git git:(trunk) git log --oneline -5 c7b9731 (HEAD -> trunk) A + 4 958f33f (origin/trunk) A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk a9cb358 no conflict d8a2f95 Merge branch 'trunk' of ../hello-git into trunk You will see our A + 4 and you will see origin's A + 3 "underneath" or previous in history. Note There is no merge commit. People really seem to like this cleaner history.

The Problem of Rebase Don't forget, rebase replays the commits on top of the history change. Here is the linked git status call from above ➜ remote-git git:(958f33f) git status interactive rebase in progress; onto 958f33f Last command done (1 command done): pick a1cbe6c A + 4 No commands remaining. You are currently rebasing branch 'trunk' on '958f33f'. (all conflicts fixed: run "git rebase --continue") Changes to be committed: (use "git restore --staged ..." to unstage) modified: bar.md Thought Experiment Ask yourself, where is README.md?

Answer Well, since we accepted theirs (marked as ours in the resolution) (we will talk about ours vs theirs later) changeset, we technically removed any change from README.md . Therefore there was no change to rebase continue on. Remember, we have origin/trunk effectively checked out. therefore there is no change to README.md when we accept our changes during a rebase Problem Lets try create the same issue except this time lets accept ours ( theirs by

position (bottom)) change.

  1. A + 5 in hello-git
  2. A + 6 in remote-git
  3. git pull origin trunk --rebase in remote-git to cause the conflict
  4. accept A + 6 change and git rebase --continue
  5. check out history to see A + 6 commit Solution remote-git ➜ remote-git git:(trunk) git diff diff --git a/README.md b/README.md index 43b4231..0c72736 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 3 +A + 5

D E remote-change ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 6' [trunk 6740bc7] A + 5 1 file changed, 1 insertion(+), 1 deletion(-) hello-git ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 5' [trunk fac2b82] A + 6 1 file changed, 1 insertion(+), 1 deletion(-) rebase ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

  • 094cca8...fac2b82 trunk -> origin/trunk (forced update) Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 6740bc7... A + 5 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply 6740bc7... A + 5 ➜ remote-git git:(6a3fb28) vim README.md

➜ remote-git git:(6a3fb28) git status interactive rebase in progress; onto fac2b82 Last commands done (2 commands done): pick c7b9731 A + 4 pick 6740bc7 A + 5 No commands remaining. You are currently rebasing branch 'trunk' on 'fac2b82'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(6a3fb28) git add . ➜ remote-git git:(6a3fb28) git rebase --continue A + 5

  1. Please enter the commit message for your changes. Lines starting
  2. with '#' will be ignored, and an empty message aborts the commit.

  1. interactive rebase in progress; onto fac2b82
  2. Last commands done (2 commands done):
  3. pick c7b9731 A + 4
  4. pick 6740bc7 A + 5
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on 'fac2b82'.

  1. Changes to be committed:
  2. modified: README.md

[detached HEAD 52bfa5a] A + 5 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated refs/heads/trunk.

check history ➜ remote-git git:(6a3fb28) git log --oneline -5 52bfa5a (HEAD -> trunk) A + 5 6a3fb28 A + 4 fac2b82 (origin/trunk) A + 6 958f33f A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk This is where rebase can suck This is where things get complicated. We have kept our change in the rebase. (our being remote-git's trunk) Problem Create a change in hello-git and pull again.

  1. Add a NewLine below A + 6 in hello-git
  2. rebase pull hello-git and cause the conflict
  3. DO NOT RESOLVE THE CONFLICT

Solution ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git diff diff --git a/README.md b/README.md index c04f8e6..18b7811 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ A + 6 +NewLine D E remote-change ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'small new line' [trunk 99df23c] small new line 1 file changed, 1 insertion(+) Now lets pull from remote-git with rebase

➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 328 bytes | 328.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

fac2b82..99df23c trunk -> origin/trunk Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 52bfa5a... A + 5 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Could not apply 52bfa5a... A + 5 Weird... lets check the conflict ➜ remote-git git:(abcc4f4) git status interactive rebase in progress; onto 99df23c Last commands done (2 commands done): pick 6a3fb28 A + 4 pick 52bfa5a A + 5 No commands remaining. You are currently rebasing branch 'trunk' on '99df23c'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a"

Ok, only README.md is conflicted. A + 6 NewLine D E remote-change downstream change Wait... didn't we already resolve this conflict? Why are we resolving it again? Thought Exercise Does this now make sense with how rebase works?

Answer Rebase works by replaying the commits one at a time. Therefore if we have our change from a conflict and then we replay the changes we will reconflict on the same change again and again. Does that mean rebase sucks? Well no, it keeps your history very clean, but does that mean rebase can be annoying? Yes. Question (i know there is an active conflict right now due to rebase)

Given what you know now, would you use rebase or merge? Answer I would... and there is likely something you don't know about

RERERE rerere is just one of the strangest options in all of git. There are some basic commands that can be ran. Check out the man page, man git-rerere to go into details. I have never needed them before, i just use the config and live my best life From the The Git Docs The git rerere functionality is a bit of a hidden feature. The name stands for “reuse recorded resolution” and, as the name implies, it allows you to ask Git to remember how you’ve resolved a hunk conflict so that the next time it sees the same conflict, Git can resolve it for you automatically. Problem

Enable rerere just for this project. You can enable it globally if you like it. Make the config option rerer.enabled to true Solution git config rerere.enabled true and to validate git config --list --local ... a few options ... rerere.enabled=true

What is rerere? rerere stands for REuse REcorded REsolution. Or in other words, git will automagically remember how you handled a specific conflict and will just replay your decision the next time you run into it. It is not all sunshine and rainbows. You can, refer to man git-rerere , delete rerere's in case you incorrectly resolved a conflict

Problem Resolve the conflict and accept our change, A + 6 Solution ➜ remote-git git:(abcc4f4) vim README.md ➜ remote-git git:(abcc4f4) git status interactive rebase in progress; onto 99df23c Last commands done (2 commands done): pick 6a3fb28 A + 4 pick 52bfa5a A + 5 No commands remaining. You are currently rebasing branch 'trunk' on '99df23c'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged ..." to unstage)

(use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(abcc4f4) git add . ➜ remote-git git:(abcc4f4) git rebase --continue [detached HEAD 226add3] A + 5 1 file changed, 1 insertion(+), 2 deletions(-) Successfully rebased and updated refs/heads/trunk. Problem To test out our rerere, lets create one more change to hello-git and see if we can auto play the conflict resolution For this add a small change to upstream.md .

Solution ➜ hello-git git:(trunk) vim upstream.md ➜ hello-git git:(trunk) git diff [trunk 980fe2d] yaya 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upstream.md b/upstream.md index b39813a..5691d02 100644 --- a/upstream.md +++ b/upstream.md @@ -1 +1 @@ -upstream change +upstream change -- yaya ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'yaya' now in remote-git ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 248 bytes | 248.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

99df23c..980fe2d trunk -> origin/trunk ➜ remote-git git:(trunk) git log --oneline -5 Successfully rebased and updated refs/heads/trunk. c28b45c (HEAD -> trunk) A + 5 2107110 A + 4 980fe2d (origin/trunk) yaya 99df23c small new line fac2b82 A + 6 Lets go! Our conflict this time was auto played for us! Ours and Theirs Sometimes during a conflict you just want to choose the entire file from one side or the other of a conflict. Typically in an editor this is a very simple task, but it can as easily be done from the command line.

Ours and Theirs "Its just the worst with git" - Some Coding Guy Ours Vs Theirs • Ours is the change of the current branch

• Theirs is the change of the incomming branch To select theirs or ours use the following checkout command git checkout --ours README.md #use "ours" change git checkout --theirs README.md #use "theirs" change Problem / Setup Lets create a conflict again, in README.md

  1. hello-git make it A + 7
  2. remote-git make it A + 8
  3. merge from upstream and resolved the conflict with "ours"
  4. validate you have merged the changes via git log

Solution ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 8' [trunk 6ec352b] A + 7 1 file changed, 1 insertion(+), 1 deletion(-) ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 7' [trunk f5b13f5] A + 8 1 file changed, 1 insertion(+), 1 deletion(-) Lets do a merge instead of a rebase and pull from origin in remote-git ➜ remote-git git:(trunk) git pull origin trunk remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 323 bytes | 323.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

980fe2d..f5b13f5 trunk -> origin/trunk Auto-merging README.md

CONFLICT (content): Merge conflict in README.md Recorded preimage for 'README.md' Automatic merge failed; fix conflicts and then commit the result. Now, lets resolve this by selecting our change. ➜ remote-git git:(trunk) git checkout --ours README.md Updated 1 path from the index ➜ remote-git git:(trunk) git status On branch trunk You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m "merged" Recorded resolution for 'README.md'. [trunk ec6930d] merged We have now officially "merged" with ours, and you can verify this by opening up README.md and looking at the contents. A + 7 D E remote-change downstream change You can even see the change in the log for A + 8, but we still maintain 7 ➜ remote-git git:(trunk) git log --oneline -5 --graph

ec6930d (HEAD -> trunk) merged

|
| * f5b13f5 (origin/trunk) A + 8

| 6ec352b A + 7

| c28b45c A + 5

| 2107110 A + 4

|/ A good reminder

  1. you don't want to mix merge and rebase. I typically just try to stick with rebasing my branch and ff-merge on public branches.
  2. long lived feature branches just suck. rerere helps, but they still suck Lets do this again, but use rebase. But before we do, since have mixed merge and rebase, lets push our changes to hello-git or else things will get hairy quickly.

Don't forget Don't forget to change branches in hello-git ➜ hello-git git:(trunk) git checkout bar Switched to branch 'bar' push to hello-git ➜ remote-git git:(trunk) git push origin trunk Enumerating objects: 16, done. Counting objects: 100% (15/15), done. Delta compression using up to 12 threads Compressing objects: 100% (8/8), done. Writing objects: 100% (10/10), 1.05 KiB | 1.05 MiB/s, done. Total 10 (delta 1), reused 0 (delta 0), pack-reused 0 To ../hello-git f5b13f5..ec6930d trunk -> trunk

Problem Perform the same task as before except with rebase

  1. hello-git make it A + 9
  2. remote-git make it A + 10
  3. rebase from upstream and resolved the conflict with "ours."
  4. do not git rebase --continue Solution remote-git

➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'A + 10' [trunk 120df5e] A + 9 1 file changed, 1 insertion(+), 1 deletion(-) hello-git ➜ hello-git git:(trunk) vim README.md ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git commit -m 'A + 9' [trunk d53a122] A + 10 1 file changed, 1 insertion(+), 1 deletion(-) Now lets pull again from remote but with --rebase enabled ➜ remote-git git:(trunk) git pull origin trunk --rebase remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 315 bytes | 315.00 KiB/s, done From ../hello-git

branch trunk -> FETCH_HEAD

ec6930d..d53a122 trunk -> origin/trunk Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not apply 120df5e... A + 9 hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm ", then run "git rebase --continue" hint: You can instead skip this commit: run "git rebase --skip" hint: To abort and get back to the state before "git rebase" Recorded preimage for 'README.md' Could not apply 120df5e... A + 9 Perfect, we now can select ours via git checkout

➜ remote-git git:(d53a122) git checkout --ours README.md Updated 1 path from the index Before we commit, lets take a look at the contents A + 9 D E remote-change downstream change wait... a second. That isn't the contents we wanted! We wanted ours. What happened? Note Well remember the rebase operations

  1. checkout the branch we are rebasing on. (that means checkout hello git)
  2. replay our changes on top of that updated branch one at a time

So that means when we hit a conflict: • ours IS hello-git , the branch we are replaying on. • theirs IS remote-git , the commits we are replaying one at a time. It is the easiest part of git to screw up and i personally remember it as "rebase is backwards." Or you can remember the steps of rebase and come to the same conclusion. Problem Instead of using ours use theirs then --continue the rebase.

Solution ➜ remote-git git:(d53a122) git checkout --theirs README.md Updated 1 path from the index Now lets inspect the contents A + 10 D E remote-change downstream change nice git add && rebase --continue to move on

Interactive Rebase There is ackshually more to rebase? Yes. Lets talk about interactive rebases, which are quite useful. The primary use case is squashing which can be very nice for history you can also edit individual commit messages and more, but i haven't ever really done that in my 10+ years of git'ing Problem Setup the repo with 3 sample commits on remote-git . To make things easy, make the commit message something like "added 1 to the end" and add 1 to the end of README.md

Solution ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 1 to the end' [trunk 9ebedbd] Added 1 to the end 1 file changed, 1 insertion(+) ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 2 to the end' [trunk 8456d89] Added 2 to the end 1 file changed, 1 insertion(+) ➜ remote-git git:(trunk) vim README.md ➜ remote-git git:(trunk) git add . ➜ remote-git git:(trunk) git commit -m 'Added 3 to the end' [trunk f000c2e] Added 3 to the end 1 file changed, 1 insertion(+) The contents of README.md A + 9 D E remote-change

downstream change 1 2 3 Interactive Rebase Steps To begin an interactive rebase we need to provide a point in time to rebase with. Typically, the simplest way to do this is with HEAD . HEAD1 means one commit back from HEAD . Since we did 3 commits we would use HEAD~3 to select the base where we were before our 3 commits. git rebase -i That means rebase , interactively, to the current commit

( HEAD in this case) You will be presented an editor with all the options. Read them carefully. Commitish? Yes, an odd word, but it makes sense when you think about it. There is a whole language to describe commits, HEAD1 is a very common version of this. That means with rebase you could provide the exact commit, or a relative path to the commit hash (HEAD1) Problem

SQUASH the three commits you made into one commit. This will require you to execute the rebase command, a HEAD , and read the text that appears to understand how to squash. It may take one or more tries. Remember, if you goof up you can always use reflog to get back to the original commit you started at. validate that you have created a squashed commit out of the 3 commits Solution execute the following: git rebase -i HEAD3

This means we will interactive rebase the last 3 commits. You should get presented with the following pick 9ebedbd Added 1 to the end pick 8456d89 Added 2 to the end pick f000c2e Added 3 to the end

  1. Rebase 9f67690..f000c2e onto 9f67690 (3 commands)

  1. Commands:
  2. p, pick = use commit
  3. r, reword = use commit, but edit the commit message
  4. e, edit = use commit, but stop for amending
  5. s, squash = use commit, but meld into previous commit
  6. f, fixup [-C | -c] = like "squash" but keep only the previous
  7. commit's log message, unless -C is used, in which # keep only this commit's message; -c is same as -C # opens the editor
  8. x, exec = run command (the rest of the line) using shell
  9. b, break = stop here (continue rebase later with 'git rebase --continue')
  10. d, drop = remove commit
  11. l, label
  12. t, reset
  13. m, merge [-C | -c ]
  14. . create a merge commit using the original merge commit's
  15. . message (or the oneline, if no original merge commit was
  16. . specified); use -c to reword the commit message

  1. These lines can be re-ordered; they are executed from top to bottom.

  1. If you remove a line here THAT COMMIT WILL BE LOST.

  1. However, if you remove everything, the rebase will be aborted.

The key line in the text comments is

  1. s, squash = use commit, but meld into previous commit This means that if we replace pick with s or squash we will squash that commit, or meld into previous commit . Meaning make the previous commit and squash commit become one commit. pick 9ebedbd Added 1 to the end squash 8456d89 Added 2 to the end squash f000c2e Added 3 to the end We could have also done pick 9ebedbd Added 1 to the end s 8456d89 Added 2 to the end s f000c2e Added 3 to the end Save and exit and git will present a new screen
  2. This is a combination of 3 commits.
  3. This is the 1st commit message:

Added 1 to the end

  1. This is the commit message #2: Added 2 to the end
  2. This is the commit message #3: Added 3 to the end
  3. Please enter the commit message for your changes. Lines starting
  4. with '#' will be ignored, and an empty message aborts the commit.

  1. Date: Sun Feb 25 08:50:40 2024 -0700

  1. interactive rebase in progress; onto 9f67690
  2. Last commands done (3 commands done):
  3. squash 8456d89 Added 2 to the end
  4. squash f000c2e Added 3 to the end
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on '9f67690'.

  1. Changes to be committed:
  2. modified: README.md

Now you have the chance to create a whole new commit message for the newly combined commits. Lets edit the message slightly

  1. This is a combination of 3 commits.

  2. This is the 1st commit message: 1, 2, and 3 combined

  3. Please enter the commit message for your changes. Lines starting

  4. with '#' will be ignored, and an empty message aborts the commit.

  1. Date: Sun Feb 25 08:50:40 2024 -0700

  1. interactive rebase in progress; onto 9f67690
  2. Last commands done (3 commands done):
  3. squash 8456d89 Added 2 to the end
  4. squash f000c2e Added 3 to the end
  5. No commands remaining.
  6. You are currently rebasing branch 'trunk' on '9f67690'.

  1. Changes to be committed:
  2. modified: README.md

Once you save, you should see something similar: ➜ remote-git git:(trunk) git rebase -i HEAD~3 [detached HEAD 02d3a0f] 1, 2, and 3 combined Date: Sun Feb 25 08:50:40 2024 -0700 1 file changed, 3 insertions(+) Successfully rebased and updated refs/heads/trunk. Lets look at our logs ➜ remote-git git:(trunk) git log --oneline -7 02d3a0f (HEAD -> trunk) 1, 2, and 3 combined

9f67690 A + 9 d53a122 (origin/trunk) A + 10 ec6930d merged f5b13f5 A + 8 6ec352b A + 7 c28b45c A + 5 Look at that! Our three commits became one! Squashing can be quite an effective technique to keep the history clean and allow you to make many small commits throughout your dev cycle, preventing loss work, and then one clean commit for reviewers. I personally think this is one of the best ways to go about developing. My general workflow

  1. many small commits with a message "SQUASHME: "
  2. at the end of the dev cycle, i squash and give a proper message
  3. PR with a singular commit

← Previous Next → 4. before i PR i ensure i am at the tip of the branch and that any CI runs against latest EVERYTHING YOU'L Lm NasEteEr Dch TanOg eKsNOW

Remote Git Often we need code changes that have been created by our fellow frienemies. But how do we get their changes into our repo? Or how do we push our changes to someone else repo? It doesn't have to be remote... Often we think of remote repos as github or gitlab, but it doesn't have to be that way. If you have never used git, maybe remote is an odd term.

A remote is simply a copy of the repo somewhere else. Problem Create a new repo, remote-git . Lets initialize it as an empty git repo using git init

Solution ➜ hello-git git:(trunk) cd /path/to/project git init remote-git Initialized empty Git repository in /path/to/project/remote-git/.git/ ➜ project cd remote-git ➜ remote-git git:(trunk) Distributed Version Control • A remote is just another git repo that is of the same project and has changes we may need. To add a remote the syntax is: git remote add

Problem Add hello-git as a remote. You should name it origin Solution

➜ remote-git git:(trunk) git remote add origin ../hello-git To check you can execute git remote -v to list out your remotes and their locations ➜ remote-git git:(trunk) git remote -v origin ../hello-git (fetch) origin ../hello-git (push) Gitism There are two naming conventions i have seen: Remote is project repo

typically when you have a remote git repo its called origin . This is the source of truth repo. Remote is your fork of some other repo sometimes you have your remote repo (fork), which you will name origin and you have the project repo which is typically named upstream . NOTICE: Earlier we set our git default branch name to trunk notice that it remained even on this new project. Now we need to merge in the changes from hello-git into our new repo remote-git .

Fetch we can fetch all the git state from our remote repository by executing git fetch . This wont update the current branches checked out, just where the origin/* has them set to. Problem

Try fetching all the branches from hello-git by using git fetch Solution ➜ remote-git git:(trunk) git fetch remote: Enumerating objects: 28, done. remote: Counting objects: 100% (28/28), done. remote: Compressing objects: 100% (18/18), done. remote: Total 28 (delta 5), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (28/28), 2.15 KiB | 2.15 MiB/s, done From ../hello-git

[new branch] bar -> origin/bar

[new branch] foo -> origin/foo

[new branch] foo-rebase-trunk -> origin/foo-rebase-trunk

[new branch] trunk -> origin/trunk

[new branch] trunk-merge-foo -> origin/trunk-merge-foo

Now those branches should look familiar considering we just got done making them!

Problem Using git log can you verify that the remote's trunk has been correctly merged into our git state but the current trunk we have checked out is not up to date? Solution

First we can see our trunk has no commits ➜ remote-git git:(trunk) git log fatal: your current branch 'trunk' does not have any commits yet trunk remains in its current state origin/trunk has been updated We verify we have the remote commits in our git state ➜ remote-git git:(trunk) git log origin/trunk b23e632 (origin/trunk, origin/bar) Y 2f43452 X a665b08 E 79c5076 D cb75afe A Notice the branch names pointing to commit b23e632 are origin/trunk and origin/bar . Note IMPORTANT: Branch names can contain the remote they come from.

NOTICE if we wish to see what branch es came down with git fetch we can execute git branch -a (git branch all) to see all branches that currently exist. Anytime you see a branch that is / it means that it is the last known state of the repo's (branch). Practically what this means is that at any moment you are likely out of sync with your remote. That is ok. That is the wonders of git. You don't have to update, you can use yours until you are ready to fetch in changes. Now convenience is a real thing. I try to update regularly. The large the out of sync is, the likelihood of conflict goes north

Problem Lets update our trunk with origin/trunk . Then lets use git log to validate those commits have been merged Solution ➜ remote-git git:(trunk) git merge origin/trunk ➜ remote-git git:(trunk) Well... that wasn't very climatic was it? Lets see what we got! ➜ remote-git git:(trunk) git log --oneline b23e632 (HEAD -> trunk, origin/trunk, origin/bar) Y

2f43452 X a665b08 E 79c5076 D cb75afe A You will now see that our local trunk matches our origin/trunk Git pull Fetching is always a good idea. It keeps your local repo's remotes up to date, but it doesn't alter your state. The thing is that a lot of the time you just want the changes merged for you into your branch. This can be done with one convenient command, git pull

git pull Problem

  1. Add a line at the end of README.md in hello-git and commit it with message A remote change .
  2. Execute git pull in remote-git
  3. Think about the error. Why does it exist?

Solution ➜ remote-git git:(trunk) cd ../hello-git ➜ hello-git git:(trunk) echo "remote-change" >> README.md ➜ hello-git git:(trunk) git add README.md ➜ hello-git git:(trunk) git commit -m "A remote change" [trunk 42afc8d] A remote change 1 file changed, 1 insertion(+) Now lets swap back to remote-git and pull in the changes instead of fetch / merge ➜ remote-git git:(trunk) git pull remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 271 bytes | 271.00 KiB/s, done From ../hello-git b23e632..42afc8d trunk -> origin/trunk There is no tracking information for the current branch. Please specify which branch you want to merge with. See git-pull(1) for details. git pull If you wish to set tracking information for this branch you can

git branch --set-upstream-to=origin/ trunk You will notice we failed. Why? The reason is that we did not setup our trunk branch to track the origin/trunk branch. Git will not automatically track state in a "remote" because that may not be what you want to do. Therefore if you git pull it wont know where to pull from since nothing has been specified. This becomes even more obvious once you have more than one remote. Its often convenient to setup tracking because we can use push and pull without specifying the target branch. Git assumes that just because two branches have the same name doesn't mean they are the same. So you need to tell git to track branches manually on preexisting branches. Problem

Read the error again and figure out how to "track" the remote branch. Once you execute the command then git pull to the origin. Use the tracking command that does not involve git pull Solution ➜ remote-git git:(trunk) git branch --set-upstream-to=origin/trunk trunk Branch 'trunk' set up to track remote branch 'trunk' from 'origin' ➜ remote-git git:(trunk) git pull # Now git pull is successful! Updating b23e632..42afc8d Fast-forward README.md | 1 + 1 file changed, 1 insertion(+) Since the changes were fetch ed with our previous pull we didn't have to re-fetch, but instead we simply merged with a "fast forward" merge strategy.

Remember all of our merge and rebase talk? Well now it really applies here. We were able to fast forward merge because we made no changes to trunk. Think about Github Hopefully this gives you some insight into what is actually happening with github/gitlab/stash. Its just another repo that everyone has agreed to commit to

What about rebase? Typically whenever I pull in changes from the remote authority repo I will rebase the changes. I do not like adding in a bunch of merge commits. This is a personal preference, but I think its a superiour one :) • A long lived branch with a bunch of merge commits is much more difficult to revert. • If every change is a single commit, then the ability to revert is very trivial. • I prefer to test my changes against the current state of master not against the current state i have fetched There is a config for it There are two strategies for rebase ing changes.

  1. add --rebase flag to a pull. git pull --rebase
  2. edit your config to make this behavior the default behavior. (good thing we know about how the config works) ➜ remote-git git:(trunk) git config --add --global pull.rebase Please don't update that setting now though Now we rebase remote code every time. You don't have to set this if you don't like the idea of rebasing from authority Git !pull The opposite of pull?

Yes, git push If you wish take your changes and move the remote repo, you can do this by using git push . Much like pull, if you are not "tracking" then you cannot simply git push but instead you will have to specify the remote and branch name. Lets make some changes to bar and push them to hello-git Fun Facts • git push : allows you to push and have it received with a different name • git push : will delete a branch on the remote

Problem Create a single commit, "CHANGE FROM REMOTE", as a one line change to the end of README.md to branch bar . Then push the changes back to hello-git validate the change made it to hello-git 's bar branch Solution ➜ remote-git git:(trunk) git checkout bar Branch 'bar' set up to track remote branch 'bar' from 'origin' Switched to a new branch 'bar' ➜ remote-git git:(bar) echo "Change from remote" >> README.md ➜ remote-git git:(bar) git add README.md

➜ remote-git git:(bar) git commit -m 'CHANGE FROM REMOTE' [bar aab17e0] CHANGE FROM REMOTE 1 file changed, 1 insertion(+) NOTICE When we checked out bar we automatically started tracking. We didn't have to create the branch. This is because locally we do not have a bar branch, but origin does. Also origin is our ONLY remote. Therefore it makes sense to create a local branch and track the remote it came from. Now lets push our change ➜ remote-git git:(bar) git push Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 12 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 306 bytes | 306.00 KiB/s, done Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To ../hello-git b23e632..aab17e0 bar -> bar And lets see if we see this on hello-git ➜ remote-git git:(bar) cd ../hello-git ➜ hello-git git:(trunk) git log bar

← Previous Next → aab17e0 (bar) CHANGE FROM REMOTE b23e632 Y 2f43452 X a665b08 E 79c5076 D cb75afe A Well look at that! We are sharing changes now! The astute observer will notice that bar 's branch on hello-git was moved forward... why?

The repo needing to be downloaded git clone git@github.com:ThePrimeagen/git-bisect.git A Classic Problem • somewhere in the last 500 commits something has gone wrong. • To test if something has gone wrong takes several minutes or longer. This is not a common problem, but it is a problem you will run into in the real world. And it can be a complete pain to resolve if you do not know the tools you have at your disposal.

Logs One very straight forward strategy to determine where a bug began is manually reviewing logs and identifying when a file changed. Pros • If you know the file/module in which the bug exists and the file changes infrequently then logs can be a very fast method to identify the problem commit. ◦ This can be particularly useful when people use good commit messages. It will help you understand why they made the change they did • If people take seriously their commit messages and add key words then searching can provide a fast and powerful mechanism to find the correct change. Cons

• The file/module in which contains the bug changes frequently • Poor/No commit messages • You cannot boil down the bug to a specific key word (e.g. Widget) • If there are too many commits that match searching it can much more cumbersome than using other methods • You simply don't know any keywords to narrow down your search Searching with token Prepare Repo Before you begin with the exercise it will be good to be familiar with the repo.

  1. install any deps with npm i
  2. run tests with npm run test

Results You should see something similar if everything went well ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1)

Start at 15:46:36 Duration 30.46s (transform 44ms, setup 0ms, collect 18ms, tests 30.11s, environment 0ms, prepare 107ms) Observation we know our function foo and test foo are failing git log Problem Use man git-log and search for an option to search through logs. A bonus info diffs of the logs could be useful too!

hint: often search is referred to as grep Solution git log comes with a --grep option git log --grep "" This will search through the commits and look in the commit message for . Diff option -p will show the commits diff as well

Problem Can you find the wrong commit using --grep ? Solution ➜ git-bisect git:(master) git log -p --grep "foo" Shockingly, we have spotted our code. commit 3798398f377cd1722284ff30b211e3b66e218738

Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications diff --git a/src/index.js b/src/index.js index 4f35883..e8052cc 100644 --- a/src/index.js +++ b/src/index.js @@ -4,5 +4,5 @@

@return {number}

*/ export function foo(x) {

  • return x * 69;
  • return x * 70; } Now this is a "lucky" search. We happened to find it but in the real world this may not be nearly so easy. But its good to know the tool exists.

You could image that each testing run takes 30 minutes, so spending 5 minutes to potentially find the bug instead of 3 hours is a trade off i would make any day of the week. Of course this also assumes you know exactly which file has fallen apart, and more specifically for us, there isn't a simple test to fix :) Searching with filename One flaw in our previous search is that we were looking for occurrences of a word in commit messages which may not be the most efficient way to look. We know the file that is probably the problem, we could always check its history. To search files via git log

git log -p -- file1 file2... Problem Use file mechanism in git log to see if it helps the search

Solution git log -p -- src/index.js You may not notice, but this method only shows changes to that specific file instead of the full patch change. This allows for a bit faster perusing of the file and its change. Again, in this trivial example, it is easy to spot the offending commit! Note A nice part about file changes is that sometimes you may find a bug was caused due to another bug, a hydrabug. Perhaps there was a production issue and a jira ticket linked to a change that is now causing a new issue. History can be nice to search through.

Your Batgit belt Its good to have these in your back pocket even if you don't use them for years. There is always that some point in the future where all the sudden this will be very useful. The nice part is that all the information you need is in man git-log -S allows you to search for text in the change itself! Bisect But log searching just simply may fail or the code is complex enough that it isn't possible to simply look and understand. So what do we do? Well we need to search through the repository for the offending commit that changed the code.

To perform bisect you need two things to be true Property 1 All commits are ordered. They are ordered by time. Property 2 You know a commit that the issue is not present or can find it easily enough The reason property 1 is so important is the following: • If there is a problem that is currently plaguing the project and you go back 10 commits and observe the problem is still there. You don't have to check 9 commits back, or 8 back, you know that the problem currently exists and 10 commits ago its present. Therefore its present betwixt HEAD and 10 commits ago

This implies a very important concept

| | a ---------- unknown ----------- b If a is working commit, and b doesn't work, select the middle commit

between a and b , call it c

| | a -- unknown -- c -- unknown -- b

Problem given:

| | a -- unknown -- c -- unknown -- b if c turns out to be a commit that passes the test (it works) what observation can we make about the above graphic? Solution All commits between a and c are good. Which results in the following

graph

| | a --- good --- c --- unknown --- b

Problem given

| | a -- unknown -- c -- unknown -- b If c fails, what observations can we made about the original graph?

Solution All commits between c and b are bad. Which results in the following graph

| | a --- unknown --- c --- bad --- b Observation we can repeat this process to find our offending commit that broke the test! To repeat we need to select new bounds. So instead of a and b , we repeat with the bounds of

• if c is good, c and b • if c is bad, a and c We have cut our search space in half! If you don't recognize this algorithm it is the same principal that guides binary search. Free Algorithms Course I consider this my best course i have ever created and its free FOREVER Algorithms on FEM

Pros • You need not to know anything about the bug and you don't have to rely on commit messages. Simply a failing test case or a way to reproduce the bug manually or programmatically • It is the fastest way to search a sorted space • You don't have to be searching only for a bug but for any change in your project. You can also use this to find where a bug got fixed, where a performance regression happened, or anything else you can think of. Cons • not really any... Unless its a trivial bug that works via log searching this is pretty much the best possible option Performing bisect Git bisect requires you to have a last known good commit and a last known bad commit. From there it is able to perform the binary search.

The last known bad commit doesn't have to be best fit and neither does the last known good commit. The point is that bisect does the searching, not you. O(log n) • 1 = 1 search • 2 = 1 search • 4 = 2 searches • 8 = 3 searches • 16 = 4 searches • 32 = 5 searches • 64 = 6 searches • 128 = 7 searches ... The Basics of Bisect

  1. start git bisect git bisect start

  2. set the known bad commit git bisect bad , uses the current one

  3. set the known good commit git bisect good

  4. test

  5. git bisect <good | bad> depending on how the test runs

  6. goto 4 until git tells you the commit Problem Use the first commit of the repo and the current tip of master and find the problematic commit via git bisect. Solution

  7. Starts the process ➜ git-bisect git:(master) git bisect start

  8. Sets current commit as the bad commit. You can manually select any commit ➜ git-bisect git:(master) git bisect bad

  9. Find the initial commit ➜ git-bisect git:(master) git log --oneline

  10. Set the last known good commit to the first commit

➜ git-bisect git:(master) git bisect good b56ed57

  1. Test to see if we are working or not ➜ git-bisect git:(master) npm run test ... Test Files 1 failed (1) Tests 1 failed (1) Start at 10:49:35 Duration 30.34s (transform 24ms, setup 1ms, collect 9ms, tests 30.11s, Looks like we have a bad commit so lets execute the following:
  2. This tells git that we have a bad commit ➜ git-bisect git:(8bf2c77) git bisect bad Bisecting: 5 revisions left to test after this (roughly 3 steps) [0562cd710d40d23b5928747a45d71d2a76443752] F
  3. notice that my shell shows i have changed commits ➜ git-bisect git:(0562cd7) Lets test again! Keep on testing until we have found the commit. ✓ src/index.spec.js (1) 30041ms ✓ foo 30040ms Test Files 1 passed (1) Tests 1 passed (1) Start at 10:53:21 Duration 30.26s (transform 26ms, setup 0ms, collect 12ms, tests 30.04s, Ok we have made great success, therefore we need to tell git that we are successful. ➜ git-bisect git:(0562cd7) git bisect good Bisecting: 2 revisions left to test after this (roughly 2 steps) [eb867402c2e4678887c8aa322c0da3ef54851f7c] I

➜ git-bisect git:(eb86740) Again, git checks out the next correct branch. Lets keep running until we find the offending commit. Once you have successfully ran to the end, this is what you should see. ➜ git-bisect git:(3798398) git bisect bad 3798398f377cd1722284ff30b211e3b66e218738 is the first bad commit commit 3798398f377cd1722284ff30b211e3b66e218738 Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) And that is the offending commit! To finish up with your session, execute the following command. ➜ git-bisect git:(3798398) git bisect reset Previous HEAD position was 3798398 feat: altered foo to meet new specificati Switched to branch 'master'

Git Bisect - Automated Git bisect is great, but its a bit manual... right? If only there was a way to automate it In steps git bisect run . Effectively the exit code

  1. same setup as before with start, good, bad git bisect run This will continue to run until we find the proper commit. The run command uses the command provided and determines a good and bad commit by exit code Problem

Use git bisect run ./node_modules/.bin/vitest --run to automate the testing and find the same problem commit Solution ➜ git-bisect git:(master) git bisect start ➜ git-bisect git:(master) git log --oneline ➜ git-bisect git:(master) git bisect good b56ed57 ➜ git-bisect git:(master) git bisect bad ➜ git-bisect git:(8bf2c77) git bisect run ./node_modules/.bin/vitest --running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30107ms × foo 30106ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1) Start at 20:15:58 Duration 30.54s (transform 45ms, setup 0ms, collect 13ms, tests 30.11s, Bisecting: 5 revisions left to test after this (roughly 3 steps) [0562cd710d40d23b5928747a45d71d2a76443752] F running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ✓ src/index.spec.js (1) 30101ms ✓ foo 30099ms Test Files 1 passed (1) Tests 1 passed (1) Start at 20:16:29 Duration 30.59s (transform 48ms, setup 0ms, collect 11ms, tests 30.10s, Bisecting: 2 revisions left to test after this (roughly 2 steps) [eb867402c2e4678887c8aa322c0da3ef54851f7c] I running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect

✓ src/index.spec.js (1) 30070ms ✓ foo 30068ms Test Files 1 passed (1) Tests 1 passed (1) Start at 20:17:00 Duration 30.59s (transform 76ms, setup 0ms, collect 13ms, tests 30.07s, Bisecting: 0 revisions left to test after this (roughly 1 step) [972fa2ab45e5041a1fe8c95f31b520bc62d7af85] this commit is certainly not running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30080ms × foo 30079ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1)

Tests 1 failed (1) Start at 20:17:31 Duration 30.60s (transform 106ms, setup 0ms, collect 18ms, tests 30.08s, Bisecting: 0 revisions left to test after this (roughly 0 steps) [3798398f377cd1722284ff30b211e3b66e218738] feat: altered foo to meet new running './node_modules/.bin/vitest' '--run' RUN v1.2.1 /home/ThePrimeagen/personal/git-bisect ❯ src/index.spec.js (1) 30111ms × foo 30111ms ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ FAIL src/index.spec.js > foo AssertionError: expected 140 to deeply equal 138

  • Expected
  • Received
  • 138
  • 140 ❯ src/index.spec.js:6:20 4| test("foo", async () => { 5| await (new Promise(res => setTimeout(res, 30000))); 6| expect(foo(2)).toEqual(2 * 69); | ^ 7| }, 35000); 8| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed (1) Tests 1 failed (1) Start at 20:18:02 Duration 30.57s (transform 27ms, setup 0ms, collect 9ms, tests 30.11s, 3798398f377cd1722284ff30b211e3b66e218738 is the first bad commit

← Previous Next → commit 3798398f377cd1722284ff30b211e3b66e218738 Author: ThePrimeagen ThePrimeagen@netflix.com Date: Fri Feb 16 13:20:16 2024 -0700 feat: altered foo to meet new specifications src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) bisect found first bad commit% ➜ git-bisect git:(3798398) What. That was easy!

Git reset This has such a wide range of responsibilites from get rid of what is in the worktree or the index to walking back a commit Soft Git reset soft can be very useful if you need to make a commit that is partially finished and you want to edit that commit and change the contents ➜ hello-git git:(trunk) git log -p --oneline -1 7488b35 (HEAD -> trunk) Revert "E" diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md

+++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change Now lets say we need to continue to edit our previous commit. We have two options.

  1. we could make changes and use commit --amend . If you are not familiar with git commit --amend it allows you to meld the current staged changes into the previous commit and edit the message.
  2. we could use git reset --soft HEAD~1 to move trunk back one commit and alter the index and worktree to match the contents of the commit. Note Both operations will change the sha since we are changing the graph fundamentally

Problem Use git reset --soft HEAD1 to move trunk back one commit. This should make the working state of your branch contain the changes of the revert instead of the revert commit being in the graph inspect state of your git branch via git log and git status git commit back the reverted change with a new message Solution ➜ hello-git git:(trunk) git reset --soft HEAD1

➜ hello-git git:(trunk) git log -p --oneline -1 d53a122 (HEAD -> trunk) A + 10 diff --git a/README.md b/README.md index 68dae75..eb42c13 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A + 7 +A + 10 D E remote-change Notice log -p shows us that the current commit (-1) is not the revert. We have successfully moved back trunk ! now check out the git status! ➜ hello-git git:(trunk) git status On branch trunk Changes to be committed: (use "git restore --staged ..." to unstage) modified: README.md Its the revert we just got done doing! ➜ hello-git git:(trunk) git diff --staged diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change

commit the soft reset contents ➜ hello-git git:(trunk) git commit -m 'its not just a revert anymore' [trunk ce41293] its not just a revert anymore 1 file changed, 1 deletion(-) ➜ hello-git git:(trunk) git log --oneline -2 ce41293 (HEAD -> trunk) its not just a revert anymore d53a122 A + 10 NOTE Obviously this is dangerous as we have just altered history of a branch The basics of soft is to reset the history to the point you want (HEAD~1) and the index and worktree will contain the changes whence you came (Our revert in this example). We could have used soft reset and went back several commits and we would have all their changes.

Hard Hard will do the same thing as soft except it will drop changes to the index and worktree. That means any work that is being tracked by git will be destroyed Problem

Before we try walking back a commit, lets first create a local change and see what happens when we execute git reset --hard Add a small change to README.md and create a new file, foo.md , with any change you see fit, then execute git reset --hard which will destroy index and worktree changes Solution ➜ hello-git git:(trunk) echo "HELLO" >> README.md ➜ hello-git git:(trunk) echo "Foo" > foo.md ➜ hello-git git:(trunk) git status On branch trunk Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory) modified: README.md Untracked files: (use "git add ..." to include in what will be committed) foo.md

no changes added to commit (use "git add" and/or "git commit -a" Note • foo.md is not tracked by git • README.md is tracked and the changes are present in the worktree ➜ hello-git git:(trunk) git reset --hard HEAD is now at ce41293 its not just a revert anymore ➜ hello-git git:(trunk) git status On branch trunk Untracked files: (use "git add ..." to include in what will be committed) foo.md nothing added to commit but untracked files present (use "git add" Notice that it destroyed all work to README.md. That is because --hard will reset ALL work in the worktree and index (staging area). So even if we had git add README.md it would still have been reset back to the HEAD state. foo.md did not get destroyed because git is not tracking the file in any way.

Problem Destroy foo by using git add and git reset --hard Solution ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git reset --hard HEAD is now at ce41293 its not just a revert anymore ➜ hello-git git:(trunk) git status On branch trunk nothing to commit, working tree clean Notice that now we destroyed our work with foo.md. That is because we started to track foo.md by adding to the index (staging area). Now when we called git reset --hard it destroyed that work. NOTE CAUTION WITH RESET HARD

It can destroy your work and it can be virtually impossible to get back that work. Git reset --hard with HEAD~1 We can walk back our tree just like soft, except we discard the changes

Problem Try it now. Try walking back our commit with git reset --hard HEAD1 Solution ➜ hello-git git:(trunk) git reset --hard HEAD1 HEAD is now at d53a122 A + 10 ➜ hello-git git:(trunk) git status On branch trunk nothing to commit, working tree clean Not only did we reset trunk one step back, we also discarded all changes.

Challenge Question! Can you restore hello-git to the position it was BEFORE we started this section? Solution

➜ hello-git git:(trunk) git reflog ... 14 other entries ... # i did extra work here 7488b35 (tag: git-reset-start) HEAD@{15}: commit: Revert "E" So we can see our previous commit with Revert "E" . Lets move trunk back to there. ➜ hello-git git:(trunk) git checkout 7488b35 Note: switching to '7488b35'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to HEAD is now at 7488b35 Revert "E" ➜ hello-git git:(7488b35) git branch -D trunk Deleted branch trunk (was d53a122). ➜ hello-git git:(7488b35) git checkout -b trunk Switched to a new branch 'trunk' ➜ hello-git git:(trunk) Look at that. We have checked out that point in time, deleted our current version of trunk , and then checked out a new version of trunk.

Note There are more things you can do with git reset but i find i use restore and git reset --hard only. Git reset --hard to restore me back to HEAD cleanly and restore to remove a staged file.

EVERY←TH PIrNeGvio YuOs U'LL NEED TO KNOW ABOUT GIT NeGxITt G→UD RESET Reset – Everything You'll Need to Know About Git https://theprimeagen.github.io/fem-git/lessons/git-gud/reset

The tools I use Thanks t-pope for this lovely plugin • add and inspect change • commit • diff and diff view • push • conflict • stash • rebase

← Previous Next →

Stash is not that great Worktrees can prevent one of the most annoying problems of using git. Stashing. But wait... did we not just learn stashing? Yes. So is it bad? No. ... I am confused Worktree You are working on feature branch foo_bar . You are making great progress and you are in flow state. Just then, slack pings, lo and behold an emergency investigation is needed on

main .

  1. You can git add . any non tracked files to the index (staging area) and then stash this change then change branches to do the investigation.
  2. You can commit this change and change branches
  3. Use Worktrees What is a worktree? Generally when we say worktree we mean a linked working tree When you git init a repo it creats the main working tree . A linked working trees is a just another tree much like main working tree just without all the git history within the .git directory. In fact, the .git directory is not a directory at all, but just a file pointing to the main working tree directory.

Worktree Operations Add ➜ git worktree add The basename of the path will be used as the branch name. List ➜ git worktree list Delete There are a couple ways to go about this

  1. you can use git and git worktree remove ../foo-bar
  2. you can use rm -rf and git worktree prune Benefits

they are cheap to make since they don't need .git history. They are just a pointer. They are just slightly more expensive than a branch. But you get a commit to work on that is outside your main working tree. That means if you need to pivot quickly you don't have to commit, stash, or anything, just create a new linked working tree! Cons if each branch has a high setup cost. like if your npm install takes 100 minutes due to everything-js dependency, worktrees can be more of a pain. Problem create a worktree of hello-git with the path name ending in foo-bar . Suggestion, ../foo-bar when completed check out .git found in the linked working tree. What's different about this .git vs a main working tree .git?

Solution ➜ hello-git git:(trunk) git worktree add ../foo-bar Preparing worktree (new branch 'foo-bar') HEAD is now at 7488b35 Revert "E" you will notice that when you change into ../foo-bar that the branch name is the path's basename ➜ hello-git git:(trunk) cd ../foo-bar ➜ foo-bar git:(foo-bar) Checking out .git ➜ foo-bar git:(foo-bar) ls -la total 24 drwxrwxr-x 2 ThePrimeagen ThePrimeagen 4096 Feb 29 19:00 . drwxrwxr-x 128 ThePrimeagen ThePrimeagen 4096 Feb 29 19:00 .. -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 59 Feb 29 19:00 bar.md -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 65 Feb 29 19:00 .git -rw-rw-r-- 1 ThePrimeagen ThePrimeagen 41 Feb 29 19:00 README.md

-rw-rw-r-- 1 ThePrimeagen ThePrimeagen 24 Feb 29 19:00 upstream.md .git is a file! what! ➜ foo-bar git:(foo-bar) cat .git gitdir: /home/ThePrimeagen/personal/hello-git/.git/worktrees/foo-bar .git file shows that it is just a pointer to the main working tree. That is how it knows how to find all of the information and why you can switch branches in a linked working tree. Your edits are making edits to the main working tree's .git state In other words, working trees are "light weight clones" This means you NEVER have to stash again, if you don't want to. You can also check out an existing branch with git worktree add Problem

Go back to hello-git and list out the your linked working tree Solution ➜ foo-bar git:(foo-bar) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar]

Problem Delete the working tree with rm -rf and see if you can discover through .git that your worktree no longer is active. Then delete the worktree Solution ➜ hello-git git:(trunk) rm -rf ../foo-bar ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar] prunable [foo-bar] prunable git tells you that foo-bar is prunable (its been deleted outside of git!) That means to delete, just run prune ➜ hello-git git:(trunk) git worktree prune

➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] How did git know it was prunable? ➜ hello-git git:(trunk) ls -la .git/worktrees total 12 drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 . drwxrwxr-x 9 mpaulson mpaulson 4096 Mar 20 12:27 .. drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 foo-bar ➜ hello-git git:(trunk) ls -la .git/worktrees/foo-bar total 32 drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 . drwxrwxr-x 3 mpaulson mpaulson 4096 Mar 20 12:27 .. -rw-rw-r-- 1 mpaulson mpaulson 6 Mar 20 12:27 commondir -rw-rw-r-- 1 mpaulson mpaulson 37 Mar 20 12:27 gitdir -rw-rw-r-- 1 mpaulson mpaulson 24 Mar 20 12:27 HEAD -rw-rw-r-- 1 mpaulson mpaulson 289 Mar 20 12:27 index drwxrwxr-x 2 mpaulson mpaulson 4096 Mar 20 12:27 logs -rw-rw-r-- 1 mpaulson mpaulson 41 Mar 20 12:27 ORIG_HEAD the gitdir no longer exists, therefore we can prune this worktree!

Problem Recreate a worktree and delete it through .git this time. Solution ➜ hello-git git:(trunk) git worktree add ../foo-bar Preparing worktree (checking out 'foo-bar') HEAD is now at 7488b35 Revert "E" ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] /home/ThePrimeagen/personal/foo-bar 7488b35 [foo-bar] ➜ hello-git git:(trunk) git worktree remove ../foo-bar ➜ hello-git git:(trunk) git worktree list /home/ThePrimeagen/personal/hello-git 7488b35 [trunk] ➜ hello-git git:(trunk) ls .. | grep foo-bar ➜ hello-git git:(trunk)

I love worktrees I think they are one of the best ways to use git as they allow you to keep state based on each branch rather than on directory. This allows for partial changes just to remain partial changes instead of doing the commit, stash, or rebase squash dance. I would highly recommend getting use to working with them.

← Previous Next → Other Downsides Its not all upside when it comes to git worktrees. • rust can easily eat up a couple gigs per branch • npm installing on each branch can take a minute • go is great • .env files can be a bit of a pain

Named locations within Git History At some point there are changes in which represents a version of your sofware you want named. This could be a version you are releasing as an open source library or an internal note for the public version of a website Git has you covered with tags What is a tag? The best way to think about a tag is a branch that cannot be changed. It can only be deleted

How to use tags git tag # to create git tag -d # delete git tag # to list git checkout # to checkout a tag

Problem Create a tag List out your tags Do tags show up in log ? Solution ➜ hello-git git:(trunk) git tag my-first-tag # creates a tag at the current ➜ hello-git git:(trunk) git tag # list tags my-first-tag ➜ hello-git git:(trunk) git log --oneline -3 d0ba67f (HEAD -> trunk, tag: my-first-tag) ci work d53a122 A + 10 ec6930d merged They do show up! Even with a nice tag: prefix If you check out a tag, you go into detached HEAD . This is because tags are immutable and you cannot change them, so checking them out is no different

than checking out a commit by its sha ➜ hello-git git:(trunk) git checkout my-first-tag Note: switching to 'my-first-tag'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to HEAD is now at d0ba67f ci work ➜ hello-git git:(d0ba67f)

Git push You can push tags to a remote via git push --tags and pull via git pull --tags Problem Pull your tag to remote-git

Solution the --tags will fetch branches as well as tags ➜ remote-git git:(foo-bar-baz) git pull --tags remote: Enumerating objects: 15, done. remote: Counting objects: 100% (15/15), done. remote: Compressing objects: 100% (7/7), done. remote: Total 11 (delta 2), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (11/11), 951 bytes | 951.00 KiB/s, From ../hello-git

[new branch] baz -> origin/baz

[new branch] foo-bar -> origin/foo-bar

d53a122..d0ba67f trunk -> origin/trunk

  • 4f452da...e6d9d4b trunk2 -> origin/trunk2 (forced update)

[new tag] my-first-tag -> my-first-tag

Already up to date. ➜ remote-git git:(foo-bar-baz)

← Previous Next →

The Decision We All Face Often you have one of two choices to make: push forward or roll back. Rolling back can often require you to revert changes made to the main branch. Note In case you are confused about revert and restore This is different than git restore since we are not restoring a file to a previous commit, but instead we are commiting an inverted commit to the graph to effectively "remove" a commit. If the commit is matter, git revert is the anti-matter

Git Revert Reverting is simple. You just need to provide the commit(ish). git revert Problem Navigate to hello-git project and revert commit with log message E . Before removing it, check the contents of the commit via log with -p !

Solution One thing we could do ➜ hello-git git:(trunk) git log -p --grep E d53a122 (HEAD -> trunk) A + 10 ec6930d merged f5b13f5 A + 8 6ec352b A + 7 c28b45c A + 5 2107110 A + 4 980fe2d yaya 99df23c small new line fac2b82 A + 6 958f33f A + 3 b51e34a Merge branch 'trunk' of ../hello-git into trunk a9cb358 no conflict d8a2f95 Merge branch 'trunk' of ../hello-git into trunk 9648be0 A + 1 6eb0a42 A + 2 7282922 greatest changes 6849c67 upstream changes 42afc8d A remote change b23e632 Y 2f43452 X

a665b08 E 79c5076 D cb75afe A Using log -p to look at the contents of E using --grep and -p ➜ hello-git git:(trunk) git log -p --grep E commit a665b08996994c2e6620a6367b0ab524be221cb2 Author: ThePrimeagen the.primeagen@gmail.com Date: Sun Jan 28 10:56:37 2024 -0700 E diff --git a/README.md b/README.md index b2f0a9a..d3045e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ A D +E Revert E ➜ hello-git git:(trunk) git revert a665b08 Auto-merging README.md CONFLICT (content): Merge conflict in README.md error: could not revert a665b08... E hint: After resolving the conflicts, mark them with hint: "git add/rm ", then run hint: "git revert --continue". hint: You can instead skip this commit with "git revert --skip" hint: To abort and get back to the state before "git revert" hint: run "git revert --abort".

HALT Yes, conflicts can happen while reverting. Lets fix the conflict and finish this revert. Resloving them are a lot like rebase. figure out the code you want to keep and git revert --continue ➜ hello-git git:(trunk) git status On branch trunk You are currently reverting commit a665b08. (fix conflicts and run "git revert --continue") (use "git revert --skip" to skip this patch) (use "git revert --abort" to cancel the revert operation) Unmerged paths: (use "git restore --staged ..." to unstage) (use "git add ..." to mark resolution) both modified: README.md no changes added to commit (use "git add" and/or "git commit -a" git status shows the conflict is within README.md

A + 10 D <<<<<<< HEAD E remote-change downstream change

parent of a665b08 (E) You can see right away that one change set contains E remote-change downstream change and the other contains nothing (our revert commit). This means that git cannot tell how to revert this commit cleanly. We can help by keeping the contents below E but removing E

Problem Finish the revert by fixing the conflict then use git revert --continue use git log to see what the revert looks like Solution Merge conflict fixed

A + 10 D remote-change downstream change Now, just like rebase, we have to git revert --continue ➜ hello-git git:(trunk) git add . ➜ hello-git git:(trunk) git revert --continue Revert "E" This reverts commit a665b08996994c2e6620a6367b0ab524be221cb2.

  1. Conflicts:
  2. README.md
  3. Please enter the commit message for your changes. Lines starting
  4. with '#' will be ignored, and an empty message aborts the commit.

  1. On branch trunk
  2. You are currently reverting commit a665b08.

  1. Changes to be committed:
  2. modified: README.md

[trunk 7488b35] Revert "E" 1 file changed, 1 deletion(-) The commit message contains a nice message explaining exactly what commit you revert if any history looker decides to peruse the commits. A git log -p -1 shows the contents of the change too commit 7488b357e64cf426eaa3390a6c406e2d10f63f40 (HEAD -> trunk) Author: ThePrimeagen ThePrimeagen@netflix.com Date: Sun Feb 25 20:35:16 2024 -0700

← Previous Next → Revert "E" This reverts commit a665b08996994c2e6620a6367b0ab524be221cb2. diff --git a/README.md b/README.md index eb42c13..38dc2c1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ A + 10 D -E remote-change downstream change

← Previous Why did I not cover git diff I think that is best done with a program. Command line diffs are just hard to check out.

Git WHOAMI The name is ThePrimeagen Twitch ThePrimeTimeagen - Engineering News ThePrimeagen - Hand Crafted

The Goal To teach your git well. Not all the commands, but enough that you git it Before we git we man man has all the info

Lets execute the following command. This will open up the man page for man. If you don't know, man stands for manual. man man Or to see all the git commands ➜ fem-git git:(main) man git- # press tab for auto complete with zsh zsh: do you wish to see all 147 possibilities (49 lines)? There is a surprisingly long manual for how to read the friendly manual (RTFM), so to keep things short i'll give you some short cuts you need to know and that is it.

• j goes one line down • k goes one line up • d goes one half page down • u goes one half page up • / will search for term • n goes to next search term • N goes to prev search term EXERCISE In man pages you see bold terms. Search for the term bold and find out what it means. Here is an example, from man man , of bolded terms SYNOPSIS man [man options] [[section] page ...] ... man -k [apropos options] regexp ... man -K [man options] [section] term ... man -f [whatis options] page ... man -l [man options] file ... man -w|-W [man options] page ...

Solution bold text type exactly as shown. italic text replace with appropriate argument. [-abc] any or all arguments within [ ] are optional. -a|-b options delimited by | cannot be used together. argument ... argument is repeatable. [expression] ... entire expression within [ ] is repeatable.

What is git? Git is a distributed version control system, VCS. Instead of the traditional centralized control system where even checking out a file required admin priviledges, git allows any work to be locally and may diverge as much as you want. Git In Git, commands are divided into high level ("porcelain") commands and low level ("plumbing") commands.

We will primarily use the high level commands, but will dip down into the low level commands to really understand how git works. Key Terms • repo: a git tracked project • commit: A point in time representing the project in its entirety. ◦ The sha that represents a commit, 40 a-f 0-9 characters, is calculated from contents of change, author, time, and more • index: I will use this term or staging area interchangeably. From the github blog The Git index is a critical data structure in Git. It serves as the “staging

area” between the files you have on your filesystem and your commit history. When you run git add , the files from your working directory are hashed and stored as objects in the index, leading them to be “staged changes”. • squash: to take several commits and turn it into one commit ◦ technically a squash would be taking N commits and turning it into N - 1 to 1 commit. but typically its N commits to 1 commit • work tree, working tree, main working tree: This is your git repo. This is the set of files that represent your project. Your working tree is setup by git init or git clone . • untracked, staged, and tracked: (use excalidraw)

  1. untracked files. this means files that are not staged for the first time (indexed) or already committed / tracked by the repo. These files are the easiest to accidentally lose work on since git does not have any information about these files.

  2. indexed / staged: this is where the changes are to be committed. You must stage before you commit and you stage changes by using the git add command. see man git-add for more information

  3. tracked. These are files that are already tracked by git. Now a file could be tracked AND have staged changes (changes stored in the index) ready to be committed. • remote: The same git repo on another computer or directory. You can accept changes from or potentially push changes too. Think github Key Facts About Git • git is an acyclic graph, meaning the following cannot exist • in git, each commit is a node in the graph, and each pointer is the child to parent relationship

• if you delete untracked files they are lost forever. commit early, commit often, you can always change history to make it one commit (squashing) • man git- for the friendly manual A Warning A lot of peoples experience with git can be summed up in these five commands • push • pull • add • commit • status

Everything else is ... mysterious and painful (if this is you, fantastic, this course is MOSTLY designed for you) This course can take someone with zero knowledge and get them up to speed on git. That means I will be interweaving simple items with complex items throughout this course. If you try to skip a head you may miss out on a key conflict, or a way to use git log or reflog . The reality is if you are comfortable with • rebase • rerere • reflog • log • cat-file • config • reset this course is probably not for you. You already know the 97% of git.

Git State We will be doing some changes to the default settings throughout this course. git config --get --global init.defaultBranch master # or any name to show up here

  1. PLEASE DO THIS git config --global --unset init.defaultBranch Ensure that rerere isn't true git config --global rerere.enabled true git config --global --unset rerere.enabled git config --global rerere.enabled (you should do that now)

How things will be structured Every section is going to come with a set of instructions and time for you to do this by yourself then i'll walk through doing it myself. With git, the best way to learn is to do it yourself. We will move fast If you have questions i'll ask throughout and will answer. I have been using git

Next → for 10 years so there is blind spots.