Skip to content

Instantly share code, notes, and snippets.

@thatkookooguy
Last active July 1, 2024 11:21
Show Gist options
  • Save thatkookooguy/e8b2e32abe9818b14246fc3bb4686147 to your computer and use it in GitHub Desktop.
Save thatkookooguy/e8b2e32abe9818b14246fc3bb4686147 to your computer and use it in GitHub Desktop.

Lesson: Benefits of Small Commits (with unit-tests included)

Introduction

In this lesson, we'll explore the benefits of working with small commits and maintaining functional code at every step. We'll use a simple TypeScript calculator project and add basic operations one by one, pairing each change with its corresponding unit test using Jest. After completing the initial development, we'll introduce git bisect to demonstrate how small commits make debugging easier.

The calculator project should reflect the same stack we're using: Typescript, and jest. The code should be added into a src folder which can be built using typescipt, and the scripts in package.json should also reflect the same scripts we usually work with in our repos (build, start, test, and test:watch)

Ofc this example is minimal to show the git changes more clearly and our real application code usually is harder to apply these techniques, but the examples in this lesson should give you a starting point on how to approach this.

Before development

Creating sub-tasks in Jira effectively can help streamline your development process and promote small PRs. Here are some best practices for sub-tasks to help with that.

Break Down Stories/Tasks into Smaller Sub-Tasks

  • Identify Components: Break the main task into smaller, manageable components that can be individually developed and tested.
  • Clear Definition of Done: Each sub-task should have a clear definition of done to ensure it is completed properly before moving on.
  • Logical Progression: Ensure sub-tasks follow a logical sequence that mirrors the development workflow.

Set Clear Goals for Each Sub-Task

  • Single Responsibility Principle: Each sub-task should focus on a single aspect or feature. Avoid combining multiple changes in one sub-task.
  • Detailed Descriptions: Provide detailed descriptions, acceptance criteria, and relevant documentation for each sub-task to avoid ambiguity.

Time-box Sub-Tasks

  • Effort Estimation: Estimate the effort required for each sub-task and ensure it’s small enough to complete in a short period, ideally within a day or two.
  • Review and Adjust: Regularly review and adjust the scope of sub-tasks to keep them small and manageable.

Prioritize and Sequence Sub-Tasks

  • Dependencies: Identify and document any dependencies between sub-tasks. Ensure they are prioritized and sequenced accordingly.
  • Critical Path: Focus on the critical path first—sub-tasks that unblock other work should be prioritized.

Collaborate and Communicate:

  • Team Discussions: Involve the team in the breakdown process to ensure everyone understands the scope and goals of each sub-task.
  • Regular Updates: Maintain open communication about the progress of sub-tasks. Use comments and status updates in Jira to keep everyone informed.

Integrate with Development Workflow:

  • Branching Strategy: Align your branching strategy with Jira sub-tasks. Create separate branches for each sub-task to facilitate small, focused PRs.

Review and Refine:

  • Retrospectives: Conduct regular retrospectives to review how well the sub-task breakdown is working and identify areas for improvement.
  • Feedback Loop: Encourage feedback from the team on the size and scope of sub-tasks to continually refine the process.

Summary: BASICS for Creating Effective Sub-Tasks

BASICS is an easy-to-remember acronym for creating effective sub-tasks in Jira to promote small PRs.

Letter Step Name Step Description
B Break Down Tasks Divide main tasks into manageable sub-tasks.
A Assign Clear Goals Ensure each sub-task has a specific goal and detailed description.
S Set Time-Box Keep sub-tasks small and quickly completable.
I Identify Dependencies Document and prioritize dependencies.
C Collaborate Discuss and update progress with the team regularly.
S Sequence and Integrate Align branching strategy with sub-ttasks and use PR templates.

By following the BASICS framework, you can streamline your development process and promote small, manageable PRs.

Developing with Small Commits and Unit Tests

Benefits of small commits

  1. Isolated Changes:
    • Each commit introduces a small, focused change.
    • Easier to review and understand.
  2. Immediate Feedback:
    • Each commit includes tests, ensuring that new code works as expected.
    • Issues are caught early.
  3. Easy Reverts: If a bug is introduced, you can revert specific commits without losing unrelated work.
  4. Clean History: A clear and understandable commit history makes it easier to track changes and debug issues.

Some techniques and best practices

  • Planning in Advance: As discussed in the part 02, breaking down features into smaller, manageable sub-tasks before starting development, makes each sub-task correspond to a single branch and PR.

  • Create Branches Early: As soon as you finish a specific sub-task, create a separate branch for the next sub-task or sub-feature. This helps in isolating changes and makes it easier to create focused PRs. After the 1st branch is merged to develop, you should be able to rebase the 2nd branch onto the develop branch and open the next PR when you finish that branch.

  • Single Responsibility:

    1. Each commit should have a single responsibility, making it easier to understand and review.
    2. Avoid mixing different types of changes (e.g., feature implementation, refactoring, and bug fixes) in a single commit.
  • Descriptive Commit Messages:

    1. Use clear and descriptive commit messages that summarize the purpose of the commit.
    2. Follow a consistent commit message format, such as the semantic commit structure, to maintain clarity and organization in the commit history.
  • Test-Driven Development: Write tests alongside code changes to ensure that each commit is functional and verifiable.

  • Commit Empty New Files with Basic Wrappers for new files: Start by committing empty files with basic class or function wrappers and test descriptions. This approach makes it easier to cherry-pick commits and reorder changes as needed.

    Example:

    // src/calculator.ts
    export class Calculator {
      add(a: number, b: number) {
      }
    
      subtract(a: number, b: number) {
      }
    
      multiply(a: number, b: number) {
      }
    
      divide(a: number, b: number) {
      }
    }

    Using Jest test.todo for Planned Tests

    Jest supports the test.todo feature, allowing you to write placeholders for tests that you plan to implement later. This is useful for outlining the tests in advance and ensuring that the structure of your test suite is ready.

    Example:

    // tests/calculator.test.ts
    import { Calculator } from '../src/calculator';
    
    describe('Calculator', () => {
      test.todo('adds 2 + 3 to equal 5');
      test.todo('subtracts 5 - 3 to equal 2');
      test.todo('multiplies 2 * 3 to equal 6');
      test.todo('divides 6 / 3 to equal 2');
    });
  • After files are created:

    1. Pairing the implementation of a method with its corresponding test in the same commit ensures that each commit is both functional and verifiable.
    2. This practice reinforces the habit of writing tests alongside code changes, promoting a test-driven development (TDD) approach.
    3. if this still creates a big PR, implementation and tests can be splitted into 2 different commits for a single metho, but should generally be sequencial commits in order to verify the new code ASAP in the same commit sequence

Summary: B.R.A.N.C.H. for Developing with Small Commits and Unit Tests

B.R.A.N.C.H. is an easy-to-remember acronym for developing with small commits and unit tests.

Letter Step Name Step Description
B Break Down Features Plan in advance and break down features into smaller, manageable sub-tasks.
R Review Isolated Changes Create branches early for each sub-task and ensure each commit has a single responsibility.
A Add Descriptive Messages Use clear and descriptive commit messages and follow a consistent commit message format.
N Nest Tests with Code Write tests alongside code changes, pairing method implementation with its corresponding test in the same commit.
C Commit Empty Wrappers Start with empty files and basic class or function wrappers; use Jest’s test.todo for planned tests.
H History Clean and Revertible Maintain a clear and understandable commit history, ensuring commits are functional and verifiable.

By following the B.R.A.N.C.H. framework, you can streamline your development process, making it easier to manage, review, and debug your code.

Certainly! Here's the lesson incorporating your suggestions:


Using Git Bisect to Identify Problematic Commits

Introduction to Git Bisect

Git Bisect is a powerful tool that allows developers to efficiently locate the commit that introduced a bug or issue in the codebase. By performing a binary search through the commit history, Git Bisect can quickly narrow down the problematic commit, saving time and effort compared to manually inspecting each commit.

Benefits of Using Git Bisect

  1. Efficient Bug Identification:
    • Git Bisect automates the process of finding the commit that introduced a bug, making it faster and more accurate than manual inspection.
  2. Works with Buildable Tested Commits:
    • When each commit is small, buildable, and tested, Git Bisect can easily isolate the faulty commit without additional debugging.
  3. Saves Time:
    • The binary search approach significantly reduces the number of commits to check, making the process quicker.

How Git Bisect Works

Git Bisect operates using a binary search algorithm. It requires you to mark one commit as "good" (a commit where the code worked correctly) and one commit as "bad" (a commit where the bug is present). Git then checks out a commit halfway between these two points, and you determine whether this commit is "good" or "bad." This process continues until Git Bisect identifies the exact commit that introduced the issue.

Steps to Use Git Bisect

1. Start the Bisect Session

Begin by telling Git you want to start a bisect session:

git bisect start

2. Mark the Bad Commit

Identify and mark the commit where the bug is present:

git bisect bad <bad_commit_hash>

3. Mark the Good Commit

Identify and mark the last known good commit:

git bisect good <good_commit_hash>

4. Run Tests in Watch Mode

In a separate terminal, run your tests continuously to quickly determine the state of each checked-out commit:

npm run test:watch

5. Bisecting

Git will check out a commit halfway between the good and bad commits. Determine whether this commit is "good" or "bad" based on the test results:

  • If the tests pass, mark the commit as good:
    git bisect good
  • If the tests fail, mark the commit as bad:
    git bisect bad

6. Repeat Until Identification

Repeat the process of marking commits as good or bad based on test results. Git will continue to narrow down the range until it identifies the commit that introduced the issue.

7. Finish the Bisect Session

Once Git Bisect identifies the problematic commit, end the bisect session:

git bisect reset

Understanding Binary Search with an Example

Binary search is an efficient algorithm for finding an item from a sorted list of items. It works by repeatedly dividing in half the portion of the list that could contain the item, until you have narrowed down the possible locations to just one.

Example

I have a random number in the range of 1 to 25. You try to determine what's my number in the least number of guesses. Whenever you take a guess, I'll tell you if my number is higher or lower.

The trick is to always pick the number halfway between the possible range. Let's say my number is 5.

  1. 0th step: Guess 13 - I'll say lower.
  2. 1st step: Guess 6 - I'll say lower.
  3. 2nd step: Guess 3 - I'll say higher.
  4. 3rd step: Guess 4 - I'll say higher.
  5. 4th step: Guess 5 - Correct!

Through this method, the maximum number of steps you need to take to get to the answer is given by RoundDown(Log2(N)). In this case, N is 25, so Log2(25) = 4.64, rounding down gives 4 steps maximum.

Given the Log function, the efficiency of this algorithm increases as N gets larger:

  • Log2(1000) = 9.97
  • Log2(10000) = 13.29 (only 13 guesses needed to guess the correct answer!)

Binary Search Trees (BST)

Binary Search Trees work off the same logic. Nodes are arranged such that smaller nodes are always to the left of the current node.

Here's an example BST with 15 nodes, looking for the number 5:

  1. 5 < 8: Go left.
  2. 5 > 4: Go right.
  3. 5 < 6: Go left.

Binary Search Tree Example

By following the path described above, you can efficiently locate the number 5 in the tree.

Summary: B.R.A.N.C.H. and Git Bisect

Using the B.R.A.N.C.H. framework ensures that each commit is small, buildable, and tested, making it easier to leverage tools like Git Bisect to quickly identify and resolve issues in the codebase. By maintaining a clean commit history and writing tests alongside code changes, developers can efficiently track down bugs and maintain the overall health of the project.

By following these practices, you can streamline your debugging process, saving time and effort while ensuring that your code remains reliable and maintainable.


This lesson introduces Git Bisect, explains its benefits, and provides a clear, step-by-step guide on how to use it, reinforced with an example of binary search and a visual representation of a binary search tree.

@thatkookooguy
Copy link
Author

  • warning about example (intro)
  • talk about jira subtasks
  • writing code correctly from the start
  • talk about hunks, amend and ways to change things up after writing code
  • talk about git bisect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment