1. Happy Git and GitHub for the useR(https://happygitwithr.com/)
2. Learn Git Branching (https://learngitbranching.js.org/)
3. Git - the simple guide (https://rogerdudler.github.io/git-guide/)
4. Oh,shit! Git?! (https://ohshitgit.com/)
- One of the most common ways I use relative refs is to move branches around. You can directly reassign a branch to a commit with the -f option. So something like:
git branch -f main HEAD~3
moves (by force) the main branch to three parents behind HEAD.
git rebase main
Now the work from our bugFix branch is right on top of main and we have a nice linear sequence of commits.Note that the commit C3 still exists somewhere. Now to merge the main branch onto bugFix:
- Relative commits are powerful, but we will introduce two simple ones here:
Moving upwards one commit at a time with ^
.
Moving upwards a number of times with ~<num>
.
git branch -f main HEAD~1
. force move the main branch 1 level up the current HEAD.
git checkout main
git merge C6
. merging main branch to a specific commit.
moving the bugFix branch to specific commit. git branch -f bugFix HEAD~4
git reset
- While resetting works great for local branches on your own machine, its method of "rewriting history" doesn't work for remote branches that others are using.
git revert
.
git cherry-pick
That's it! We wanted commits C2 and C4 and git plopped them down right below us. Simple as that!
- When the
interactive rebase
dialog opens, you have the ability to do two things in our educational application:
You can reorder commits simply by changing their order in the UI (via dragging and dropping with the mouse).
You can choose to keep all commits or drop specific ones. When the dialog opens, each commit is set to be included by the pick button next to it being active. To drop a commit, toggle off its pick button.
It is worth mentioning that in the real git interactive rebase you can do many more things like squashing (combining) commits, amending commit messages, and even editing the commits themselves. For our purposes though we will focus on these two operations above.
- Here's another situation that happens quite commonly. You have some changes (newImage) and another set of changes (caption) that are related, so they are stacked on top of each other in your repository (aka one after another). The tricky thing is that sometimes you need to make a small modification to an earlier commit. In this case, design wants us to change the dimensions of newImage slightly, even though that commit is way back in our history!!
git rebase -i main
and change sequence of C2 and C3
git commit --amend
git rebase -i main
(to rechange location of C2 and C3)
git checkout master
git merge caption
- Git tags support this exact use case -- they (somewhat) permanently mark certain commits as "milestones" that you can then reference like a branch. More importantly though, they never move as more commits are created. You can't "check out" a tag and then complete work on that tag -- tags exist as anchors in the commit tree that designate certain spots.
- Because tags serve as such great "anchors" in the codebase, git has a command to describe where you are relative to the closest "anchor" (aka tag). And that command is called git describe!cGit describe takes the form of:
git describe <ref>
Where is anything git can resolve into a commit. If you don't specify a ref, git just uses where you're checked out right now (HEAD).
The output of the command looks like:
<tag>_<numCommits>_g<hash>
Where tag is the closest ancestor tag in history, numCommits is how many commits away that tag is, and is the hash of the commit being described.
- Upper management is making this a bit trickier though -- they want the commits to all be in sequential order. So this means that our final tree should have C7' at the bottom, C6' above that, and so on, all in order.
git checkout bugFix
git rebase main
git checkout C4
git rebase bugFix
git checkout C5
git rebase C4'
git checkout side
git rebase C5'
git checkout another
git rebase side
git checkout main
git merge another
-
Branch one needs a re-ordering of those commits and an exclusion/drop of C5. Branch two just needs a pure reordering of the commits, and three only needs one commit transferred!
git checkout two
.
git cherry-pick C5 C4 C3 C2
-
Technically,
git clone
in the real world is the command you'll use to create local copies of remote repositories (from github for example). We use this command a bit differently in Learn Git Branching though -- git clone actually makes a remote repository out of your local one. Sure it's technically the opposite meaning of the real command, but it helps build the connection between cloning and remote repository work. -
Well, remote branches also have a (required) naming convention -- they are displayed in the format of:
<remote name>/<branch name>
Hence, if you look at a branch named o/main, the branch name is main and the name of the remote is o.
Most developers actually name their main remote origin
, not o. This is so common that git actually sets up your remote to be named origin when you git clone a repository.
- In this lesson we will learn how to fetch data from a remote repository -- the command for this is conveniently named git fetch. You'll notice that as we update our representation of the remote repository, our remote branches will update to reflect that new representation. This ties into the previous lesson on remote branches.
git fetch
downloads commits/data for all the branches.
- updating remote branches
git commit
git pull
-
git push
is responsible for uploading your changes to a specified remote and updating that remote to incorporate your new commits. Once git push completes, all your friends can then download your work from the remote.
Imagine you clone a repository on Monday and start dabbling on a side feature. By Friday you are ready to publish your feature -- but oh no! Your coworkers have written a bunch of code during the week that's made your feature out of date (and obsolete). They've also published these commits to the shared remote repository, so now your work is based on an old version of the project that's no longer relevant.
In this case, the command git push is ambiguous. If you run git push, should git change the remote repository back to what it was on Monday? Should it try to add your code in while not removing the new code? Or should it totally ignore your changes since they are totally out of date?
git pull --rebase
git push
But with git pull
then git push
Input
Output
step 1: git checkout main; git pull --rebase
step 2: git checkout side1; git rebase main
step 3: git checkout main; git rebase side1; git push
step 5: (we can also use cherry-pick) git cherry-pick C3 C4; git push
step 6: git cherry-pick C5 C6 C7; git push
Easier way to complete this:
git fetch
(fetch C8 from remote and point o/main to C8)git rebase o/main side1
(rebase side1 on top of o/main)git rebase side1 side2
git rebase side2 side3
git rebase side3 main
git push
Rebase vs. Merge
There's a lot of debate about the tradeoffs between merging and rebasing in the development community. Here are the general pros / cons of rebasing:
Pros:
Rebasing makes your commit tree look very clean since everything is in a straight line
Cons:
Rebasing modifies the (apparent) history of the commit tree. For example, commit C1 can be rebased past C3. It then appears that the work for C1' came after C3 when in reality it was completed beforehand.
Some developers love to preserve history and thus prefer merging. Others (like myself) prefer having a clean commit tree and prefer rebasing. It all comes down to preferences.
- We can do the above mentioned problem but with
git merge
$ git fetch
$ git checkout main
$ git pull
$ git merge side1
$ git push
$ git merge side2
$ git push
$ git merge side3
$ git push
- You can make any arbitrary branch track o/main, and if you do so, that branch will have the same implied push destination and merge target as main. This means you can run git push on a branch named totallyNotMain and have your work pushed to the main branch on the remote!
There are two ways to set this property. The first is to checkout a new branch by using a remote branch as the specified ref. Running
git checkout -b totallyNotMain o/main
Creates a new branch named totallyNotMain and sets it to track o/main.
Another way to set remote tracking on a branch is to simply use the git branch -u option. Running
git branch -u o/main foo
will set the foo branch to track o/main. If foo is currently checked out you can even leave it off:
git branch -u o/main
Input
Output
$ git checkout -b side
$ git commit
$ git branch -u o/main
$ git fetch
$ git rebase o/main
$ git push
git push can optionally take arguments in the form of:
git push <remote> <place>
Ex: git push origin main
translates to this in English:
Go to the branch named "main" in my repository, grab all the commits, and then go to the branch "main" on the remote named "origin". Place whatever commits are missing on that branch and then tell me when you're done.
By specifying main as the "place" argument, we told git where the commits will come from and where the commits will go. It's essentially the "place" or "location" to synchronize between the two repositories. Keep in mind that since we told git everything it needs to know (by specifying both arguments), it totally ignores where we are checked out!
Input
Output
git push origin main
git push origin foo
You might then be wondering -- what if we wanted the source and destination to be different? What if you wanted to push commits from the foo branch locally onto the bar branch on remote? n order to specify both the source and the destination of , simply join the two together with a colon:
git push origin <source>:<destination>
This is commonly referred to as a colon refspec. Refspec is just a fancy name for a location that git can figure out (like the branch foo or even just HEAD~1).
Input
Output
step 1: git push origin foo:main
step 2: git push origin main^:foo
Solved!
The arguments for git fetch are actually very, very similar to those for git push. It's the same type of concepts but just applied in the opposite direction (since now you are downloading commits rather than uploading).
If you specify a place with git fetch like in the following command:
git fetch origin foo
Git will go to the foo
branch on the remote, grab all the commits that aren't present locally, and then plop them down onto the o/foo
branch locally.
You might be wondering -- why did git plop those commits onto the o/foo
remote branch rather than just plopping them onto my local foo branch? I thought the parameter is a place that exists both locally and on the remote?
Well git makes a special exception in this case because you might have work on the foo
branch that you don't want to mess up!! This ties into the earlier lesson on git fetch
-- it doesn't update your local non-remote branches, it only downloads the commits (so you can inspect / merge them later).
"Well in that case, what happens if I explicitly define both the source and destination with <source>:<destination>
?"
If you feel passionate enough to fetch commits directly onto a local branch, then yes you can specify that with a colon refspec. You can't fetch commits onto a branch that is checked out, but otherwise git will allow this.
Here is the only catch though --
That being said, developers rarely do this in practice. I'm introducing it mainly as a way to conceptualize how fetch and push are quite similar, just in opposite directions.
Git Alias
Edit ~/.gitconfig to add these useful developer shortcuts.
[core]
editor = code --wait
[alias]
co = checkout
cob = checkout -b
cp = cherry-pick
go = clone --recurse-submodules -j8
kill = branch -D
last = log -1 HEAD
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
ll = log --pretty=oneline --graph --name-status
pr = !git fetch --all --prune && git prune && git remote prune origin
spull = !git fetch --all && git pull --rebase && git submodule sync --recursive && git submodule update --init --recursive
spush = push --recurse-submodules=on-demand
sup = submodule update --recursive --init
sync = !git fetch --all && git pull --rebase origin master
undo = reset HEAD~1 --mixed
unstage = reset HEAD --
wipe = clean -Xfd
amend = commit --amend
[diff]
submodule = log
[fetch]
recurseSubmodules = on-demand
[init]
templatedir = ~/.git-templates
[pull]
rebase = true
[status]
submoduleSummary = true
[rerere]
enabled = true
Want to squash commits before merge request
This will create a interactive rebase against master
branch.
naz@pop-os ~/git-cloned-repos/psf-vsn/sources/row-guidance (naz/save-data-csv)
$ git rebase -i origin/master
Or you can do interactive rebase against a specific commit:
$ git rebase -i commit-hash
Then you pick all the commits that you want to squash as s
and leave the commit
as pick
where you want to squash all the commits.
Manually do accept incoming change
or accept current change
if git can't figure out which one to accept. Then:
git rebase --continue
Then you have to force push the new commits on your branch at origin
$ git push -f origin naz/save-data-csv
Messed Something Up!
git reflog
This will show all the changes made with commit. Find the commit hash where you were correct. Then
$ git checkout commit-hash -b backup/dar/naz
This will create a new branch named backup/dar/naz with the commit where you were correct. Delete the branch where you messed up.
git kill messed-up-branch
Now, you want to change the name of the current branch (backup/dar/naz) into old brach(messed-up-branch), because old branch is probably pushed to origin and you want to follow that that or keep comments of merge request.
Be on your current branch and rename it to old branch:
git branch -m messed-up-branch
Then force push this branch to origin where the original branch was:
git push -f origin messed-up-branch