COMS 4995 Advanced Systems Programming

Git Overview and Internals

Basic Workflow Refresher

$ # Creating a brand new repository from scratch
$ mkdir myrepo
$ cd myrepo
$ git init
Initialized empty Git repository in /home/hans/tmp/myrepo/.git/
$ # Running git init creates a .git directory, we'll take a peek inside later.
$ ls -alF
total 2
drwx------ 3 hans hans  3 Dec 26 16:58 ./
drwx------ 3 hans hans  3 Dec 26 16:58 ../
drwx------ 7 hans hans 10 Dec 26 16:58 .git/
$ # Create a new file in our empty git repo.
$ vim hello.c
$ cat hello.c
#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello ASP!\n");
}
$ # Check the status of the repo. We are on the main branch (by default) and we
$ # haven't made any commits yet. hello.c is untracked from git's perspective.
$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	hello.c

nothing added to commit but untracked files present (use "git add" to track)
$ # Add hello.c to the staging area via git add.
$ git add hello.c
$ # Now that we have staged changes, we can commit them.
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   hello.c

$ # Create the root commit of the main branch (i.e. no parent commit). git
$ # status reports a clean working tree since there are no more untracked
$ # files or changes to tracked files.
$ git commit -m "Add hello.c"
[main (root-commit) 16932c8] Add hello.c
 1 file changed, 5 insertions(+)
 create mode 100644 hello.c
$ git status
On branch main
nothing to commit, working tree clean
$ # Make a change to hello.c
$ vim hello.c
$ cat hello.c
#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello ASP! xD\n");
}
$ # git detects an unstaged change to hello.c.
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   hello.c

no changes added to commit (use "git add" and/or "git commit -a")
$ # Run git diff to compare working directory to the staging area, or to the
$ # last commit if the staging area is empty.
$ git diff
diff --git a/hello.c b/hello.c
index adac03b..c5c96cf 100644
--- a/hello.c
+++ b/hello.c
@@ -1,5 +1,5 @@
 #include <stdio.h>
 
 int main(int argc, char **argv) {
-    printf("Hello ASP!\n");
+    printf("Hello ASP! xD\n");
 }
$ # Stage the change.
$ git add hello.c
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   hello.c

$ # Make another change without committing.
$ vim hello.c
$ cat hello.c
#include <stdio.h>

int main(int argc, char **argv) {
    printf("Goodbye AP!\n");
    printf("Hello ASP! xD\n");
}
$ # We have both staged and unstaged changes to hello.c.
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   hello.c

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   hello.c

$ # git diff will only show the unstaged changes.
$ git diff
diff --git a/hello.c b/hello.c
index c5c96cf..e9d8cf5 100644
--- a/hello.c
+++ b/hello.c
@@ -1,5 +1,6 @@
 #include <stdio.h>
 
 int main(int argc, char **argv) {
+    printf("Goodbye AP!\n");
     printf("Hello ASP! xD\n");
 }
$ # git diff --cached compares the staging area to the previous commit, so it
$ # only show the staged changes.
$ git diff --cached
diff --git a/hello.c b/hello.c
index adac03b..c5c96cf 100644
--- a/hello.c
+++ b/hello.c
@@ -1,5 +1,5 @@
 #include <stdio.h>
 
 int main(int argc, char **argv) {
-    printf("Hello ASP!\n");
+    printf("Hello ASP! xD\n");
 }
$ # Stage the second change and commit the changes to hello.c.
$ git add hello.c
$ git commit -m "Change program output"
[main 7e1c1b1] Change program output
 1 file changed, 2 insertions(+), 1 deletion(-)
$ # We've made two commits so far. The tip of the main branch is at the second
$ # commit. The parent of the second commit is the root commit.
$ git log
commit 7e1c1b1e4233b8438d39e141d8fa7f1b366e11dd (HEAD -> main)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:01:56 2023 -0500

    Change program output

commit 16932c89ddc83057007119cbf91e10890076715f
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 16:59:33 2023 -0500

    Add hello.c

Branches and Merging

$ # Starting from the main branch with a clean working tree
$ git status
On branch main
nothing to commit, working tree clean
$ # Create a new branch called "dev" to start a new line of development that
$ # diverges from main. Maintaining stable and experimental branches is a common
$ # development workflow. For example, you could adopt the following:
$ #     - Only commit 100% fully functioning features to main.
$ #     - Work on experimental features on dev. Only merge into main once
$ #       they're ready. This means you can could break dev during development,
$ #       but always keep main stable.
$ git branch dev
$ # List all branches, note that main is still the current branch
$ git branch
  dev
* main
$ # Switch from the main branch to dev. (We could've skipped git branch dev and
$ # used instead git switch -c dev to create dev and switch to it.)
$ git switch dev
Switched to branch 'dev'
$ git branch
* dev
  main
$ # Make a change to hello.c and commit it to the dev branch.
$ vim hello.c
$ git diff
diff --git a/hello.c b/hello.c
index e9d8cf5..56df2ee 100644
--- a/hello.c
+++ b/hello.c
@@ -3,4 +3,5 @@
 int main(int argc, char **argv) {
     printf("Goodbye AP!\n");
     printf("Hello ASP! xD\n");
+    printf("Added from dev branch\n");
 }
$ git add hello.c && git commit -m "Add a line from dev"
[dev 43434c2] Add a line from dev
 1 file changed, 1 insertion(+)
$ # The dev branch is now ahead of main by one commit. In git, HEAD typically
$ # refers to the tip of the current branch (see section on detached HEAD
$ # below).
$ git log
commit 43434c2d09dfb13ca328fd82a7cbde7fba3f0440 (HEAD -> dev)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:03:03 2023 -0500

    Add a line from dev

commit 7e1c1b1e4233b8438d39e141d8fa7f1b366e11dd (main)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:01:56 2023 -0500

    Change program output

commit 16932c89ddc83057007119cbf91e10890076715f
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 16:59:33 2023 -0500

    Add hello.c
$ # Switch to main and make an unrelated change to hello.c (i.e. on a different
$ # line). Commit that change to the main branch.
$ git switch main
Switched to branch 'main'
$ git branch
  dev
* main
$ vim hello.c
$ git diff
diff --git a/hello.c b/hello.c
index e9d8cf5..6a6230e 100644
--- a/hello.c
+++ b/hello.c
@@ -1,6 +1,7 @@
 #include <stdio.h>
 
 int main(int argc, char **argv) {
+    printf("Added from main branch\n");
     printf("Goodbye AP!\n");
     printf("Hello ASP! xD\n");
 }
$ git add hello.c && git commit -m "Add a line from main"
[main 457b2df] Add a line from main
 1 file changed, 1 insertion(+)
$ # Running git log shows the three commits we've made on the main branch, that
$ # is, all the commits we made in the current branch from the latest to the
$ # oldest. This explains why we don't see the commits on dev from here.
$ # More precisely, git log list all commits that are reachable by following
$ # each commit's parent link, starting from HEAD.
$ git log
commit 457b2dfc138bc4ed89b0441e84c919db92b400c0 (HEAD -> main)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:05:00 2023 -0500

    Add a line from main

commit 7e1c1b1e4233b8438d39e141d8fa7f1b366e11dd
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:01:56 2023 -0500

    Change program output

commit 16932c89ddc83057007119cbf91e10890076715f
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 16:59:33 2023 -0500

    Add hello.c
$ # Merge the dev branch into the main branch. This will attempt to bring in the
$ # changes made on the dev branch into the main branch with a special "merge
$ # commit". We were able to "auto-merge" the two branches because the changes on
$ # them were unrelated. Had there been "merge conflicts", i.e. conflicting
$ # changes like changing the same line, we would've had to resolve them and add the
$ # conflict-resolving changes as part of the merge commit.
$ git merge dev
Auto-merging hello.c
Merge made by the 'ort' strategy.
 hello.c | 1 +
 1 file changed, 1 insertion(+)
$ # We run git log with --graph option to illustrate the following:
$ #     - HEAD points to a new commit on main, which is the merge commit
$ #     - The merge commit has two parent commits, the previous tip of main and
$ #       the tip of dev (i.e., the latest commits of the branches being merged)
$ #     - git log now shows the commits on the dev branch because they are
$ #       reachable from the merge commit
$ git log --graph
*   commit 2dd8655d376924a9bb0cb47a7b96a2dd86e7a1aa (HEAD -> main)
|\  Merge: 457b2df 43434c2
| | Author: Hans Montero <hjm2133@columbia.edu>
| | Date:   Tue Dec 26 17:05:16 2023 -0500
| | 
| |     Merge branch 'dev'
| | 
| * commit 43434c2d09dfb13ca328fd82a7cbde7fba3f0440 (dev)
| | Author: Hans Montero <hjm2133@columbia.edu>
| | Date:   Tue Dec 26 17:03:03 2023 -0500
| | 
| |     Add a line from dev
| | 
* | commit 457b2dfc138bc4ed89b0441e84c919db92b400c0
|/  Author: Hans Montero <hjm2133@columbia.edu>
|   Date:   Tue Dec 26 17:05:00 2023 -0500
|   
|       Add a line from main
| 
* commit 7e1c1b1e4233b8438d39e141d8fa7f1b366e11dd
| Author: Hans Montero <hjm2133@columbia.edu>
| Date:   Tue Dec 26 17:01:56 2023 -0500
| 
|     Change program output
| 
* commit 16932c89ddc83057007119cbf91e10890076715f
  Author: Hans Montero <hjm2133@columbia.edu>
  Date:   Tue Dec 26 16:59:33 2023 -0500
  
      Add hello.c

Detached HEAD State

Your git repo’s HEAD need not always refer to the tip of some branch. It’s possible to switch to some arbitrary commit in the git repository that isn’t necessarily associated with a branch, thereby entering the “detached HEAD state”:

$ git switch --detach <commit>

Here, you can look around the repo as it appeared at that commit and make temporary changes to run quick experiments. When you’re done, you exit the detached HEAD state by switching back to the tip of an existing branch or creating a new branch to commit those changes to.

Git Objects

$ # Add a Makefile and commit it to the main branch
$ echo "hello:" > Makefile  # a one-liner Makefile
$ make && ./hello
cc     hello.c   -o hello
Added from main branch
Goodbye AP!
Hello ASP! xD
Added from dev branch
$ git add Makefile && git commit -m "Add Makefile"
[main 5e9fd66] Add Makefile
 1 file changed, 1 insertion(+)
 create mode 100644 Makefile
$ # Make another change to hello.c and commit it.
$ vim hello.c
$ git diff
diff --git a/hello.c b/hello.c
index 0e0ec94..9b1d262 100644
--- a/hello.c
+++ b/hello.c
@@ -5,4 +5,5 @@ int main(int argc, char **argv) {
     printf("Goodbye AP!\n");
     printf("Hello ASP! xD\n");
     printf("Added from dev branch\n");
+    printf("Branching is epic!\n");
 }
$ git add hello.c && git commit -m "Add an epic line"
[main 00ea4d7] Add an epic line
 1 file changed, 1 insertion(+)
$ # git log -n 3 shows the three latest commits on the main branch, including
$ # the two we just made
$ git log -n 3
commit 00ea4d7b51a257b0efecf90e5048706962b7572e (HEAD -> main)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:07:45 2023 -0500

    Add an epic line

commit 5e9fd66313096d29c81f6926a7f80f351fa65d7a
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:06:59 2023 -0500

    Add Makefile

commit 2dd8655d376924a9bb0cb47a7b96a2dd86e7a1aa
Merge: 457b2df 43434c2
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Tue Dec 26 17:05:16 2023 -0500

    Merge branch 'dev'
$ # Let's examine the contents of the .git directory.
$ ls -alF .git
total 55
drwx------  8 hans hans  14 Dec 26 17:07 ./
drwx------  3 hans hans   6 Dec 26 17:07 ../
-rw-------  1 hans hans  17 Dec 26 17:07 COMMIT_EDITMSG
-rw-------  1 hans hans  21 Dec 26 17:04 HEAD
-rw-------  1 hans hans  41 Dec 26 17:05 ORIG_HEAD
drwx------  2 hans hans   2 Dec 26 16:58 branches/
-rw-------  1 hans hans  92 Dec 26 16:58 config
-rw-------  1 hans hans  73 Dec 26 16:58 description
drwx------  2 hans hans  15 Dec 26 16:58 hooks/
-rw-------  1 hans hans 209 Dec 26 17:07 index
drwx------  2 hans hans   3 Dec 26 16:58 info/
drwx------  3 hans hans   4 Dec 26 16:59 logs/
drwx------ 26 hans hans  26 Dec 26 17:07 objects/
drwx------  4 hans hans   4 Dec 26 16:58 refs/
$ # The objects directory stores file contents and metadata (like commits). The
$ # files here are named by the SHA-1 hash of their contents. SHA-1 (Secure Hash
$ # Algorithm 1) is a cryptographic hash function that produces a unique, 160-bit
$ # digital footprint of the input data.
$ tree .git/objects
.git/objects
├── 00
│   └── ea4d7b51a257b0efecf90e5048706962b7572e
├── 0e
│   └── 0ec94c0d2e554cd5efdb38c2d4998e5cfef72f
├── 16
│   └── 932c89ddc83057007119cbf91e10890076715f
├── ...

24 directories, 22 files
$ # Let's inspect the latest commit on main. Grab the commit hash from git log
$ # so we can run git cat-file -t, which shows the type of the git object. As
$ # expected, this is a commit object.
$ git cat-file -t 00ea4d7
commit
$ # git cat-file -p shows the contents of the git object:
$ #     - A commit is simply a snapshot of your entire working directoy. Git
$ #       represents your working directory using a tree object
$ #     - There are one or more parent commits
$ #     - The commit objects stores metadata about the commit
$ git cat-file -p 00ea4d7
tree f6c7fe0d7f6f41cd9d00214a1521176ffdeab20d
parent 5e9fd66313096d29c81f6926a7f80f351fa65d7a
author Hans Montero <hjm2133@columbia.edu> 1703628465 -0500
committer Hans Montero <hjm2133@columbia.edu> 1703628465 -0500

Add an epic line
$ # Grab the object hash of the tree at this commit and inspect it:
$ #     - The tree object is git's internal representation of a directory's
$ #       contents
$ #     - Our working directory contains two files
$ #     - For each file, the tree object stores its name, mode, and the hash of
$ #       the corresponding git blob object storing the content
$ git cat-file -p f6c7fe0
100644 blob 850742712c6609bf85aecca7c24c2eebf4d89b0a	Makefile
100644 blob 9b1d2628d4bfb5cd74a2c7138ce0572a89676554	hello.c
$ # Grab hello.c's blob object hash and inspect it. For blob objects, git
$ # cat-file -p simply dumps the file contents.
$ git cat-file -p 9b1d262
#include <stdio.h>

int main(int argc, char **argv) {
    printf("Added from main branch\n");
    printf("Goodbye AP!\n");
    printf("Hello ASP! xD\n");
    printf("Added from dev branch\n");
    printf("Branching is epic!\n");
}
$ # Verify that the blob object hash of hello.c matches the hash listed in the
$ # tree object.
$ git hash-object hello.c
9b1d2628d4bfb5cd74a2c7138ce0572a89676554
$ # Grab the hash of the second most recent commit and inspect it to find the
$ # tree object representing the state of the working directory at that commit:
$ git cat-file -p 5e9fd66
tree a7b81382a38608aef4a30a3b0447b5c5533825e4
parent 2dd8655d376924a9bb0cb47a7b96a2dd86e7a1aa
author Hans Montero <hjm2133@columbia.edu> 1703628419 -0500
committer Hans Montero <hjm2133@columbia.edu> 1703628419 -0500

Add Makefile
$ # Inspect the tree object from the second most recent commit and compare it to
$ # the tree object from the most recent commit that we previously inspected.
$ # There are still two blobs, but take note of the blob hashes. Makefile was
$ # unchanged between the two commits, so the blob hash is the same in both trees.
$ # Recall that we changed hello.c between the two commits, so the blob hashes
$ # are different -- the file contents are different.
$ git cat-file -p a7b8138
100644 blob 850742712c6609bf85aecca7c24c2eebf4d89b0a	Makefile
100644 blob 0e0ec94c0d2e554cd5efdb38c2d4998e5cfef72f	hello.c

Here’s a diagram summarizing the object relationship above: obj-diagram

Recall that each commit is a snapshot of your entire working directory, represented by a tree object. As shown above with Makefile, however, if a file didn’t change across commits, both commits will refer to the same blob object. On the other hand, git did create an entire new blob for hello.c even though there was only a one-line change. We don’t show it here, but git will periodically pack similar objects to save space.

Subdirectories

Git represents subdirectories in the repository by using nested tree objects, as demonstrated below:

$ mkdir subdir
$ echo "abc" > subdir/notes.txt
$ git add subdir/notes.txt && git commit -m "Track notes.txt"
[main 0f9198f] Track notes.txt
 1 file changed, 1 insertion(+)
 create mode 100644 subdir/notes.txt

$ git cat-file -p 0f9198f
tree 16e61aca78c5ec55f0321214d243cea470d7b165
parent 00ea4d7b51a257b0efecf90e5048706962b7572e
author Hans Montero <hjm2133@columbia.edu> 1724945605 -0400
committer Hans Montero <hjm2133@columbia.edu> 1724945605 -0400

Track notes.txt

$ # The commit's tree object shows a nested tree object representing the
$ # subdirectory!
$ git cat-file -p 16e61ac
100644 blob 850742712c6609bf85aecca7c24c2eebf4d89b0a	Makefile
100644 blob 9b1d2628d4bfb5cd74a2c7138ce0572a89676554	hello.c
040000 tree 9c5f564f8d9e058610684c3488f888d30f48a290	subdir
$ # The nested tree object contains the contents of the subdirectory
$ git cat-file -p 9c5f564
100644 blob 8baef1b4abc478178b004d62031cf7fe6db6f903	notes.txt

The updated git object diagram: obj-diagram-2

Index: Staging Revisited

Git’s internal representation of the staging area is the file .git/index. It is a binary file that contains a list of files and their corresponding blob object hashes.

$ # Inspect the current index by running git ls-files --stage. The working
$ # directory is clean, so these hashes are from the latest commit.
$ git ls-files --stage
100644 850742712c6609bf85aecca7c24c2eebf4d89b0a 0	Makefile
100644 9b1d2628d4bfb5cd74a2c7138ce0572a89676554 0	hello.c
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0	subdir/notes.txt
$ # Add a line to hello.c, run git diff, and stage the file. Note that git diff
$ # actually compares blob 9b1d262 listed in the index to the version in our working
$ # directory.
$ vim hello.c
$ git diff
diff --git a/hello.c b/hello.c
index 9b1d262..e2047c4 100644
--- a/hello.c
+++ b/hello.c
@@ -6,4 +6,5 @@ int main(int argc, char **argv) {
     printf("Hello ASP! xD\n");
     printf("Added from dev branch\n");
     printf("Branching is epic!\n");
+    printf("Inspecting the index!\n");
 }
$ git add hello.c
$ # The index is updated with the new blob for hello.c. Note that the blob
$ # e2047c4 lives in the objects directory, is referred to by the index, but isn't
$ # pointed to by any commits yet. It is only a staged change, after all.
$ git ls-files --stage
100644 850742712c6609bf85aecca7c24c2eebf4d89b0a 0	Makefile
100644 e2047c4512fa96b96ab9e39818ab7c2d3dae00ae 0	hello.c
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0	subdir/notes.txt
$ # Let's commit and move on!
$ git commit -m "Add a line about the index"
[main 7a950ac] Add a line about the index
 1 file changed, 1 insertion(+)

The updated git object diagram: obj-diagram-3

Note that the subtree is also shared between the two commits’ trees – this makes sense since the subdirectory has not been modified.

References

$ # Recall that HEAD typically refers to the tip of the current branch. It is
$ # implemented as follows:
$ #     - .git/refs/heads contains files named after branches
$ #     - Each branch file is simply the hash of the latest commit on that branch
$ #     - .git/HEAD is just a text file containing the path to the current branch
$ #       file
$ ls .git/refs/heads
dev  main
$ cat .git/refs/heads/main
7a950ace7d889f3724078fa087cf4203fb5a321c
$ cat .git/refs/heads/dev
43434c2d09dfb13ca328fd82a7cbde7fba3f0440
$ cat .git/HEAD
ref: refs/heads/main
$ # Switching branches simply means updating the HEAD reference
$ git switch dev
Switched to branch 'dev'
$ cat .git/HEAD
ref: refs/heads/dev
$ git switch main

This reveals that a branch is simply a commit (and all commits reachable through the parent links)!

The updated git object diagram: obj-diagram-4

Note that in the detached HEAD state, .git/HEAD is not a reference to branch; It will contain the hash of the commit that you are checked out to.

GitHub

Introduction

GitHub is an online service for hosting Git repositories. GitHub repos can be owned either by individual users or an organization. Having your git repo centrally hosted online enables team collaboration on the same project, as we’ll demonstrate in this section.

Hosting your repo

This section demonstrates how to create a new GitHub repository to host our locally hosted repo from the previous section. Follow the GitHub UI to create a new repository (e.g., https://github.com/new).

After hitting “Create repository”, you’ll have created a new empty repository. The last thing we need to do on the GitHub UI is copy our repository URL so that we can address it from our local repo. In this demo, we’ll copy the SSH URL. See this GitHub SSH guide for more info on setting up SSH for your GitHub account.

With the URL copied, we can now return to our command line to set up the remote.

$ # Add our GitHub repo as a remote repo named "origin".
$ git remote add origin git@github.com:hmontero1205/myrepo.git
$ # Let's push the commits on the main branch to the empty GitHub repo.
$ git push
fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin main

$ # We need to associate our local main branch with a remote branch.
$ git push --set-upstream origin main
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Delta compression using up to 4 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (24/24), 2.04 KiB | 2.04 MiB/s, done.
Total 24 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), done.
To github.com:hmontero1205/myrepo.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
$ # Git stores information about the remote in the .git/config file
$ cat .git/config 
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        url = git@github.com:hmontero1205/myrepo.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
        remote = origin
        merge = refs/heads/main

Note that this only pushed the commits from main to the remote repo. There is no dev branch on the remote repo. You can check the GitHub UI that there’s only the main branch.

$ # Make another change to hello.c
$ vim hello.c
$ # Stage changes to tracked files and commit
$ git commit -am "Time to push"
[main 188f8f8] Time to push
 1 file changed, 1 insertion(+)
$ # Git log now shows that our local main branch is ahead of the remote main
$ # branch
$ git log -n 3
commit 188f8f8661c5cdbe2a948f3300ec13f425b8aa76 (HEAD -> main)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:07:11 2024 -0400

    Time to push

commit 7a950ace7d889f3724078fa087cf4203fb5a321c (origin/main, origin/HEAD)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Thu Aug 29 11:40:06 2024 -0400

    Add a line about the index

commit 0f9198f6ff95823437659d0b1c7ea387f10a46a6
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Thu Aug 29 11:33:25 2024 -0400

    Track notes.txt
$ # The remote main branch reference still points to 7a950ac
$ cat .git/refs/remotes/origin/main 
7a950ace7d889f3724078fa087cf4203fb5a321c
$ # Run git push to synchronize the remote repo's main branch
$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 368 bytes | 368.00 KiB/s, done.
Total 3 (delta 1), reused 1 (delta 1), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:hmontero1205/myrepo.git
   7a950ac..188f8f8  main -> main
$ # Our reference to the remote main branch also got updated
$ cat .git/refs/remotes/origin/main 
188f8f8661c5cdbe2a948f3300ec13f425b8aa76

Collaborating on GitHub

In this section, we’ll demonstrate how two people can collaborate on the same project through GitHub. Recall the branching workflow discussed earlier – we are going to work on a new feature on a new branch and merge it into the main branch. Instead of merging on the command line as we did before, however, we’ll merge the branch using GitHub’s UI. This will give us the opportunity to show how to make pull requests and perform code review on GitHub.

$ # Assume we are now a teammate on a new feature. The teammate first clones the
$ # repo from GitHub into a local directory.
$ git clone git@github.com:hmontero1205/myrepo.git
Cloning into 'myrepo'...
remote: Enumerating objects: 27, done.
remote: Counting objects: 100% (27/27), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 27 (delta 7), reused 27 (delta 7), pack-reused 0
Receiving objects: 100% (27/27), done.
Resolving deltas: 100% (7/7), done.
$ # git log output below shows that the origin remote is already established and
$ # synchronized with our local main branch.
$ git log -n 1
commit 188f8f8661c5cdbe2a948f3300ec13f425b8aa76 (HEAD -> main, origin/main, origin/HEAD)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:07:11 2024 -0400

    Time to push
$ # Create and switch to a new feature branch
$ git switch -c teammate2-feature
Switched to a new branch 'teammate2-feature'
$ # Make changes to hello.c and commit them
$ vim hello.c
$ git diff
diff --git a/hello.c b/hello.c
index 9a271ce..9f976b7 100644
--- a/hello.c
+++ b/hello.c
@@ -1,6 +1,6 @@
 #include <stdio.h>

-int main(int argc, char **argv) {
+void print_messages() {
     printf("Added from main branch\n");
     printf("Goodbye AP!\n");
     printf("Hello ASP! xD\n");
@@ -9,3 +9,7 @@ int main(int argc, char **argv) {
     printf("Inspecting the index!\n");
     printf("Time to push\n");
 }
+
+int main(int argc, char **argv) {
+    print_messages();
+}
$ git commit -am "Refactor prints into helper function"
[teammate2-feature 9bd248b] Refactor prints into helper function
 1 file changed, 5 insertions(+), 1 deletion(-)
$ # The teammate2-feature branch only exists in our local repo, we need to push
$ # it to GitHub. Assuming this teammate has proper permissions for the GitHub
$ # repo, they can push it upstream. Use -u (shorthand for --set-upstream) to
$ # associate our local branch with the remote branch we're about to create on
$ # GitHub
$ git push -u origin teammate2-feature
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 390 bytes | 390.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'teammate2-feature' on GitHub by visiting:
remote:      https://github.com/hmontero1205/myrepo/pull/new/teammate2-feature
remote:
To github.com:hmontero1205/myrepo.git
 * [new branch]      teammate2-feature -> teammate2-feature
Branch 'teammate2-feature' set up to track remote branch 'teammate2-feature' from 'origin'.

At this point, let’s assume that the teammate’s feature is complete and they would like to merge it into the main branch. One way to do that would be to merge the teammate2-feature branch into the main branch locally and then pushing from the main branch to GitHub.

GitHub offers an alternate workflow:

Note that GitHub gives you three options to submit the PR:

We’ll choose the “Create a merge commit” option. See John Hui’s Yet Another Git Guide for more details on merging vs. rebasing. The GitHub UI now shows that the feature branch has been merged into the main branch.

As a teammate, let’s now switch back to our local main branch and pull down the latest version.

$ # Our local repo is not aware of the changes on the remote main branch
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git log -n 1
commit 188f8f8661c5cdbe2a948f3300ec13f425b8aa76 (HEAD -> main, origin/main, origin/HEAD)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:07:11 2024 -0400

    Time to push
$ # git pull will fetch the latest changes from the remote repo, update our
$ # local references to the remote, and merge the remote main branch into our local
$ # main branch. Since we haven't done anything on our local main branch, this is
$ # simply another fast-forward. 
$ git pull
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 2), reused 2 (delta 1), pack-reused 0 (from 0)
Unpacking objects: 100% (4/4), 1.13 KiB | 1.13 MiB/s, done.
From github.com:hmontero1205/myrepo
   188f8f8..ea9b1fc  main              -> origin/main
 * [new branch]      teammate2-feature -> origin/teammate2-feature
Updating 188f8f8..ea9b1fc
Fast-forward
 hello.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
$ git log -n 3
commit ea9b1fcfa7f0673aba31b6963af3ef60ea5ea082 (HEAD -> main, origin/main, origin/HEAD)
Merge: 188f8f8 9bd248b
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:15:16 2024 -0400

    Merge pull request #2 from hmontero1205/teammate2-feature

    Refactor prints into helper function

commit 9bd248bc364762c1bf7738d4e9beb789e08c6286 (origin/teammate2-feature)
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:13:41 2024 -0400

    Refactor prints into helper function

commit 188f8f8661c5cdbe2a948f3300ec13f425b8aa76
Author: Hans Montero <hjm2133@columbia.edu>
Date:   Sat Aug 31 21:07:11 2024 -0400

    Time to push

Last updated: 2024-08-31