Git workflow

This article walks through a practical Git workflow for a small development
team.

We will follow three developers—Alice(admin), Bob(dev), and Carol(dev).

They work on new features, handle an urgent security fix, and integrate their
work into the main branch using commands like rebase, cherry-pick.

Setup the remote repo

To simulate a remote server for this local test, we’ll create a bare repository.

1
2
3
4
5
6
7
8
9
git init --bare remote-repo.git

for user in Alice Bob Carol; do
git clone remote-repo.git $user
cd $user
git config user.name "$user"
git config user.email "$user@team.com"
cd ..
done

Alice initializes the repo

1
2
3
4
5
6
7
8
cd Alice
echo "# Team Project" > README.md
echo "node_modules/" > .gitignore
echo "package-lock.json" >> .gitignore
echo "console.log('Project started');" > index.js
git add .
git commit -m "Initial commit: project setup"
git push -u origin main

Bob adds a login feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cd ../Bob
git pull origin main
git checkout -b feature/login

echo "function login(username, password) {" > auth.js
echo ''' if (!username || !password) return false;''' >> auth.js
echo " return true;" >> auth.js
echo "}" >> auth.js

git add auth.js
git commit -m "feat(auth): add login function with validation"

echo "function validatePassword(password) {" >> auth.js
echo " return password.length >= 8;" >> auth.js
echo "}" >> auth.js

git add auth.js
git commit -m "feat(auth): add password validation"

git push -u origin feature/login

Carol adds a payment feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cd ../Carol
git pull origin main
git checkout -b feature/payment

echo "function processPayment(amount, currency) {" > payment.js
echo " return {" >> payment.js
echo " success: true," >> payment.js
echo " amount: amount," >> payment.js
echo " currency: currency" >> payment.js
echo " };" >> payment.js
echo "}" >> payment.js

git add payment.js
git commit -m "feat(payment): add payment processing"

echo "function refundPayment(transactionId) {" >> payment.js
echo " return transactionId !== null;" >> payment.js
echo "}" >> payment.js

git add payment.js
git commit -m "feat(payment): add refund functionality"

git push -u origin feature/payment

Alice pushes a new commit on main

1
2
3
4
5
6
cd ../Alice
git pull origin main
echo "const VERSION = '1.0.0';" > config.js
git add config.js
git commit -m "chore: add version configuration"
git push origin main

Bob integrates the login feature with rebase

  1. Rebase main into feature branch

    1
    2
    cd ../Bob
    git pull origin main --rebase
    • Before rebase (feature/login):

      1
      2
      3
      4
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"
      * 4e572e0 Bob feat(auth): add password validation
      * 1b9d983 Bob feat(auth): add login function with validation
      * 181e112 Alice Initial commit: project setup
    • After rebase (feature/login):

      1
      2
      3
      4
      5
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"
      * 3f58f24 Bob feat(auth): add password validation
      * ab83b58 Bob feat(auth): add login function with validation
      * 6fe7ef2 Alice chore: add version configuration
      * 181e112 Alice Initial commit: project setup
  2. Merge feature branch into main

    1
    2
    3
    git checkout main
    git merge --no-ff feature/login -m "Merge pull request #1: Add login functionality"
    git push origin main

    The main branch git log graph right now:

    1
    2
    3
    4
    5
    6
    7
    8
    ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"
    * 8c8fad5 Bob Merge pull request #1: Add login functionality
    |\
    | * 3f58f24 Bob feat(auth): add password validation
    | * ab83b58 Bob feat(auth): add login function with validation
    | * 6fe7ef2 Alice chore: add version configuration
    |/
    * 181e112 Alice Initial commit: project setup
  3. Delete unused feature branch on remote and local

    1
    2
    git push origin --delete feature/login
    git branch -d feature/login
  • Merging instead of rebasing results in a non-linear history.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    git pull origin main --no-rebase
    git log --graph --oneline --format="%h %<(16)%an %s"
    * 301189c Bob Merge branch 'main' of /home/jooooody/git_test/temp/remote-repo into feature/login
    |\
    | * c56c42b Alice chore: add version configuration
    * | df9c340 Bob feat(auth): add password validation
    * | 477ca6f Bob feat(auth): add login function with validation
    |/
    * 1dd69a3 Alice Initial commit: project setup
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    git checkout main
    git merge --no-ff feature/login -m "Merge pull request #1: Add login functionality"
    git log --graph --oneline --format="%h %<(16)%an %s"
    * a7bbfe4 Bob Merge pull request #1: Add login functionality
    |\
    | * 301189c Bob Merge branch 'main' of /home/jooooody/git_test/temp/remote-repo into feature/login
    | |\
    | | * c56c42b Alice chore: add version configuration
    | |/
    |/|
    | * df9c340 Bob feat(auth): add password validation
    | * 477ca6f Bob feat(auth): add login function with validation
    |/
    * 1dd69a3 Alice Initial commit: project setup

Alice adds more feature on main

1
2
3
4
5
6
7
8
9
10
cd ../Alice
git pull origin main

echo "function logError(error) {" > utils.js
echo " console.error(\`[\${new Date().toISOString()}] \${error}\`);" >> utils.js
echo "}" >> utils.js

git add utils.js
git commit -m "feat(utils): add error logging utility"
git push origin main

Bob hotfix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd ../Bob
git pull origin main
git checkout -b hotfix/security-patch

echo "function sanitizeInput(input) {" > security.js
echo " return input.replace(/[<>]/g, '');" >> security.js
echo "}" >> security.js

git add security.js
git commit -m "fix(security): add input sanitization"

git checkout main
git merge --no-ff hotfix/security-patch -m "Merge hotfix: Security patch for input sanitization"
git push origin main

main branch git graph:

1
2
3
4
5
6
7
8
9
10
11
12
13
╰─$ git log --graph --oneline --format="%h %<(16)%an %s"
* 287625d Bob Merge hotfix: Security patch for input sanitization
|\
| * 624d97a Bob fix(security): add input sanitization
|/
* 8d3d75b Alice feat(utils): add error logging utility
* 8c8fad5 Bob Merge pull request #1: Add login functionality
|\
| * 3f58f24 Bob feat(auth): add password validation
| * ab83b58 Bob feat(auth): add login function with validation
| * 6fe7ef2 Alice chore: add version configuration
|/
* 181e112 Alice Initial commit: project setup

Carol continues her feature but she needs the code from hotfix

  1. Cherry-pick hotfix code

    1
    2
    3
    4
    cd ../Carol
    git fetch origin
    git log --all --oneline | grep "fix(security): add input sanitization" # 624d97a
    git cherry-pick 624d97a
    • Before cherry-pick (feature/payment):

      1
      2
      3
      4
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"                                                          130 ↵
      * 25f7a59 Carol feat(payment): add refund functionality
      * 201212a Carol feat(payment): add payment processing
      * 181e112 Alice Initial commit: project setup
    • After cherry-pick (feature/payment):

      1
      2
      3
      4
      5
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"                                                          130 ↵
      * b4e1093 Bob fix(security): add input sanitization
      * 25f7a59 Carol feat(payment): add refund functionality
      * 201212a Carol feat(payment): add payment processing
      * 181e112 Alice Initial commit: project setup
  2. Continue develop

    1
    2
    3
    4
    echo "// Added fraud detection" >> payment.js
    git add payment.js
    git commit -m "feat(payment): implement fraud detection logic"
    git push origin feature/payment

Carol syncs with main using rebase

  1. Rebase:

    1
    2
    git fetch origin
    git rebase origin/main
    • Before rebase (feature/payment):

      1
      2
      3
      4
      5
      6
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"                                                          130 ↵
      * ece2c62 Carol feat(payment): implement fraud detection logic
      * b4e1093 Bob fix(security): add input sanitization
      * 25f7a59 Carol feat(payment): add refund functionality
      * 201212a Carol feat(payment): add payment processing
      * 181e112 Alice Initial commit: project setup
    • After rebase (feature/payment):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      ╰─$ git log --graph --oneline --format="%h %<(16)%an %s"
      * f19e516 Carol feat(payment): implement fraud detection logic
      * f167f6e Carol feat(payment): add refund functionality
      * 9dae6be Carol feat(payment): add payment processing
      * 287625d Bob Merge hotfix: Security patch for input sanitization
      |\
      | * 624d97a Bob fix(security): add input sanitization
      |/
      * 8d3d75b Alice feat(utils): add error logging utility
      * 8c8fad5 Bob Merge pull request #1: Add login functionality
      |\
      | * 3f58f24 Bob feat(auth): add password validation
      | * ab83b58 Bob feat(auth): add login function with validation
      | * 6fe7ef2 Alice chore: add version configuration
      |/
      * 181e112 Alice Initial commit: project setup
  2. A force push is required because rebasing rewrites history, which changes the commit hashes.

    1
    git push origin feature/payment --force-with-lease

    A rebase does not move your old commits. Instead, it re-applies them one by one on top of the target branch (main).
    Each re-applied commit is a new commit with a different parent. Since a commit’s hash is calculated based on its content, author, timestamp, and its parent, changing the parent results in a brand new hash.

Alice merges the completed payment feature

1
2
3
4
5
6
cd ../Alice
git pull origin

git merge --no-ff origin/feature/payment -m "Merge pull request #2: Add payment processing and refund functionality"
git push origin main
git push origin --delete feature/payment

Explore git history in carol’s workspace

git reflog

  • It shows a history of where the current commit (HEAD) pointer has been. It logs actions like checkout, commit, reset, and merge.
  • It lists these actions in reverse chronological order, with the newest action at the top
  • By default, it only shows the log for HEAD.
  • It’s very useful for finding lost work and recovering from mistakes in your local repository.

Carol feature/pyment branch:

1
2
3
4
5
6
7
8
9
10
11
12
git reflog | cat -n
1 f19e516 HEAD@{0}: rebase (finish): returning to refs/heads/feature/payment
2 f19e516 HEAD@{1}: rebase (pick): feat(payment): implement fraud detection logic
3 f167f6e HEAD@{2}: rebase (pick): feat(payment): add refund functionality
4 9dae6be HEAD@{3}: rebase (pick): feat(payment): add payment processing
5 287625d HEAD@{4}: rebase (start): checkout origin/main
6 ece2c62 HEAD@{5}: commit: feat(payment): implement fraud detection logic
7 b4e1093 HEAD@{6}: cherry-pick: fix(security): add input sanitization
8 25f7a59 HEAD@{7}: commit: feat(payment): add refund functionality
9 201212a HEAD@{8}: commit: feat(payment): add payment processing
10 181e112 HEAD@{9}: checkout: moving from main to feature/payment
11 181e112 HEAD@{10}: initial pull

image.png

The yellow highlights show all of the git reflog command output entry.

After rebase, commits like 201212a, 25f7a59 are gone from the branch history. They have been replaced by new commits (f19e516, f167f6e, etc.).

The old commits still exist for a while and can be found with the reflog, but they are no longer part of the branch.

What happens to the old commits in the reflog?

These “lost” commits don’t stay in your repository forever.

Git has a process called “garbage collection” (git gc) that periodically cleans up objects that are no longer reachable from any branch or tag.

The entries in reflog also expire (typically after 90 days). So, reflog is your safety net, but it’s not a permanent archive for abandoned history.

git reflog —all

  • Show all log of references (local branch, remote branch, tag, etc.)
  • Not only show current branch history, includes all branch
  • For tracking all branch history

Carol git workspace (feature/payment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
git reflog --all | cat -n
1 f19e516 refs/remotes/origin/feature/payment@{0}: update by push
2 f19e516 refs/heads/feature/payment@{0}: rebase (finish): refs/heads/feature/payment onto 287625d107994a34a4ddd9b2b6c553005f5ce4a8
3 f19e516 HEAD@{0}: rebase (finish): returning to refs/heads/feature/payment
4 f19e516 HEAD@{1}: rebase (pick): feat(payment): implement fraud detection logic
5 f167f6e HEAD@{2}: rebase (pick): feat(payment): add refund functionality
6 9dae6be HEAD@{3}: rebase (pick): feat(payment): add payment processing
7 287625d HEAD@{4}: rebase (start): checkout origin/main
8 ece2c62 refs/remotes/origin/feature/payment@{1}: update by push
9 ece2c62 refs/heads/feature/payment@{1}: commit: feat(payment): implement fraud detection logic
10 ece2c62 HEAD@{5}: commit: feat(payment): implement fraud detection logic
11 b4e1093 refs/heads/feature/payment@{2}: cherry-pick: fix(security): add input sanitization
12 b4e1093 HEAD@{6}: cherry-pick: fix(security): add input sanitization
13 287625d refs/remotes/origin/HEAD@{0}: fetch
14 287625d refs/remotes/origin/main@{0}: fetch origin: fast-forward
15 25f7a59 refs/heads/feature/payment@{3}: commit: feat(payment): add refund functionality
16 201212a refs/heads/feature/payment@{4}: commit: feat(payment): add payment processing
17 181e112 refs/heads/feature/payment@{5}: branch: Created from HEAD
18 181e112 refs/heads/main@{0}: initial pull
19 25f7a59 refs/remotes/origin/feature/payment@{2}: update by push
20 181e112 refs/remotes/origin/main@{1}: pull origin main: storing head
21 25f7a59 HEAD@{7}: commit: feat(payment): add refund functionality
22 201212a HEAD@{8}: commit: feat(payment): add payment processing
23 181e112 HEAD@{9}: checkout: moving from main to feature/payment
24 181e112 HEAD@{10}: initial pull
  • When you work with a branch that is synchronized with a remote repository, Git maintains two important and separate references in your local repository.
    • refs/heads/feature/payment
      • This is your local branch.
      • Its reflog records the history of changes made to this branch on your machine. This includes actions like making new commits (git commit) or resetting the branch (git reset).
    • refs/remotes/origin/feature/payment
      • This is a remote-tracking branch. It acts as a cache of the branch’s state on the remote server (origin).
      • Its reflog only records when this branch was updated. This happens when you communicate with the remote, for example, by running git push or git fetch.
  • When you delete the local branch with git branch -D feature/payment, you are only deleting the refs/heads/feature/payment reference.
    • The reflog file associated with your local branch is deleted along with the branch itself.
    • When you run git reflog --all, you will no longer see entries for refs/heads/feature/payment.
    • The remote-tracking branch (refs/remotes/...) is unaffected and its reflog remains, because you did not delete that reference.
    • You can see the old branch name directly in the HEAD reflog history: 181e112 (HEAD -> main) HEAD@{0}: checkout: moving from feature/payment to main
      • Even though the branch reference is gone, some commits (f19e516, etc.) still exists in your repository for a while. Because it’s part of the HEAD reflog history, the HEAD isn’t gone.
      • Then, you use that hash to recreate the branch: git branch feature/payment 181e112

git rev-list Head

  • It lists all reachable commits—that is, the HEAD and all its ancestors.
  • It works by starting at the HEAD points to and walking backward through the history by following each commit’s **parent(s).

Carol git workspace (feature/payment):

1
2
3
4
5
6
7
8
9
10
11
12
git rev-list HEAD --oneline | cat -n
1 f19e516 feat(payment): implement fraud detection logic
2 f167f6e feat(payment): add refund functionality
3 9dae6be feat(payment): add payment processing
4 287625d Merge hotfix: Security patch for input sanitization
5 624d97a fix(security): add input sanitization
6 8d3d75b feat(utils): add error logging utility
7 8c8fad5 Merge pull request #1: Add login functionality
8 3f58f24 feat(auth): add password validation
9 ab83b58 feat(auth): add login function with validation
10 6fe7ef2 chore: add version configuration
11 181e112 Initial commit: project setup

image.png


image.png

Actions like rebase or reset change where a branch reference, it means the branch reference now points to a new chain of commits, making the old commits unreachable from this reference.

git rev-list —reflog

  • The --reflog option tells rev-list to use every entry from the reflog as a starting point for its search.
  • It lists the ancestors of every commit mentioned in the reflog.
  • This command is useful for finding unreachable commits that might have been lost after operations like rebase or git reset --hard.

Carol git workspace (feature/payment):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git rev-list --reflog --oneline | cat -n
1 9dae6be feat(payment): add payment processing
2 f167f6e feat(payment): add refund functionality
3 f19e516 feat(payment): implement fraud detection logic
4 ece2c62 feat(payment): implement fraud detection logic
5 b4e1093 fix(security): add input sanitization
6 287625d Merge hotfix: Security patch for input sanitization
7 624d97a fix(security): add input sanitization
8 8d3d75b feat(utils): add error logging utility
9 8c8fad5 Merge pull request #1: Add login functionality
10 3f58f24 feat(auth): add password validation
11 ab83b58 feat(auth): add login function with validation
12 6fe7ef2 chore: add version configuration
13 201212a feat(payment): add payment processing
14 25f7a59 feat(payment): add refund functionality
15 181e112 Initial commit: project setup

image.png

Explore git history in alice’s workspace

git reflog

Carol git workspace (main branch):

1
2
3
4
5
6
7
╰─$ git reflog | cat -n
1 2131a21 HEAD@{0}: merge origin/feature/payment: Merge made by the 'ort' strategy.
2 287625d HEAD@{1}: pull origin main: Fast-forward
3 8d3d75b HEAD@{2}: commit: feat(utils): add error logging utility
4 8c8fad5 HEAD@{3}: pull origin main: Fast-forward
5 6fe7ef2 HEAD@{4}: commit: chore: add version configuration
6 181e112 HEAD@{5}: commit (initial): Initial commit: project setup

git reflog —all

Carol git workspace (main branch):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
╰─$ git reflog --all | cat -n
1 2131a21 refs/remotes/origin/main@{0}: update by push
2 2131a21 refs/heads/main@{0}: merge origin/feature/payment: Merge made by the 'ort' strategy.
3 2131a21 HEAD@{0}: merge origin/feature/payment: Merge made by the 'ort' strategy.
4 287625d refs/remotes/origin/HEAD@{0}: fetch
5 287625d refs/heads/main@{1}: pull origin main: Fast-forward
6 287625d refs/remotes/origin/main@{1}: pull origin main: fast-forward
7 287625d HEAD@{1}: pull origin main: Fast-forward
8 8d3d75b refs/heads/main@{2}: commit: feat(utils): add error logging utility
9 8c8fad5 refs/heads/main@{3}: pull origin main: Fast-forward
10 8d3d75b refs/remotes/origin/main@{2}: update by push
11 8c8fad5 refs/remotes/origin/main@{3}: pull origin main: fast-forward
12 8d3d75b HEAD@{2}: commit: feat(utils): add error logging utility
13 8c8fad5 HEAD@{3}: pull origin main: Fast-forward
14 6fe7ef2 refs/heads/main@{4}: commit: chore: add version configuration
15 6fe7ef2 refs/remotes/origin/main@{4}: update by push
16 6fe7ef2 HEAD@{4}: commit: chore: add version configuration
17 181e112 refs/heads/main@{5}: commit (initial): Initial commit: project setup
18 181e112 refs/remotes/origin/main@{5}: update by push
19 181e112 HEAD@{5}: commit (initial): Initial commit: project setup

git rev-list Head

Carol git workspace (main branch):

1
2
3
4
5
6
7
8
9
10
11
12
13
╰─$ git rev-list HEAD --oneline | cat -n                                                                          130 ↵
1 2131a21 Merge pull request #2: Add payment processing and refund functionality
2 f19e516 feat(payment): implement fraud detection logic
3 f167f6e feat(payment): add refund functionality
4 9dae6be feat(payment): add payment processing
5 287625d Merge hotfix: Security patch for input sanitization
6 624d97a fix(security): add input sanitization
7 8d3d75b feat(utils): add error logging utility
8 8c8fad5 Merge pull request #1: Add login functionality
9 3f58f24 feat(auth): add password validation
10 ab83b58 feat(auth): add login function with validation
11 6fe7ef2 chore: add version configuration
12 181e112 Initial commit: project setup

image.png

git rev-list —reflog

Carol git workspace (main branch):

1
2
3
4
5
6
7
8
9
10
11
12
13
╰─$ git rev-list --reflog --oneline | cat -n
1 2131a21 Merge pull request #2: Add payment processing and refund functionality
2 f19e516 feat(payment): implement fraud detection logic
3 f167f6e feat(payment): add refund functionality
4 9dae6be feat(payment): add payment processing
5 287625d Merge hotfix: Security patch for input sanitization
6 624d97a fix(security): add input sanitization
7 8d3d75b feat(utils): add error logging utility
8 8c8fad5 Merge pull request #1: Add login functionality
9 3f58f24 feat(auth): add password validation
10 ab83b58 feat(auth): add login function with validation
11 6fe7ef2 chore: add version configuration
12 181e112 Initial commit: project setup

The outputs of rev-list HEAD and rev-list --reflog are identical. This is because main branch has a clean, forward-only history with no rebasing or resets.