Git Advanced: Mastering Monorepos, Submodules, and Workflows – Best Practices for Coders
Dive into the best practices for managing complex Git setups with monorepos, submodules, and various development workflows, ensuring clean history, efficient collaboration, and robust codebases.
Welcome back to our 'Git Advanced' series here at CoddyKit! In our first post, we laid the groundwork for understanding advanced Git concepts. Now, in Post 2, we’re moving beyond the basics to focus on something crucial for any developer aiming for excellence: best practices. When dealing with complex setups like monorepos, managing external code with submodules, or orchestrating team collaboration through various workflows, adopting sound practices isn't just helpful – it's essential for maintaining sanity, efficiency, and code quality.
As mobile developers, you often work on projects that are part of larger ecosystems, integrate third-party libraries, and collaborate with diverse teams. Understanding how to apply best practices in Git ensures that your development process is smooth, your codebase remains stable, and your team operates like a well-oiled machine. Let’s dive in!
Monorepos: Navigating the Unified Repository Landscape with Precision
Monorepos, where multiple distinct projects reside in a single repository, offer significant advantages like simplified dependency management and atomic changes across projects. However, without best practices, they can quickly become unwieldy. Here’s how to keep yours in top shape:
1. Structured Organization is Key
- Logical Grouping: Organize your projects and components into clear, well-defined directories. Avoid a flat structure at the root. Use directories like
apps/for applications,packages/for shared libraries, andtools/for scripts. This makes navigation intuitive and reduces cognitive load. - Clear Ownership: Define clear ownership for each project or module within the monorepo. This helps streamline code reviews, bug fixes, and feature development, ensuring accountability.
- Consistent Naming Conventions: Apply consistent naming conventions for directories, files, and even commit message scopes across all projects to maintain uniformity.
2. Smart Tooling for Scalability
Monorepos thrive with the right tooling. These tools help manage dependencies, run targeted builds/tests, and enforce consistent practices:
- Build Systems: Leverage monorepo-aware build tools (e.g., Bazel, Nx for JavaScript/TypeScript, Lerna, Gradle for Android) that understand project interdependencies and can execute builds and tests only for affected projects, saving significant CI/CD time.
- CI/CD Optimization: Configure your Continuous Integration/Continuous Deployment pipelines to detect changes only in relevant projects. This prevents unnecessary full builds and speeds up feedback loops. For example, if only an iOS app changed, don't rebuild the Android app.
3. Consistent Commit Hygiene
In a monorepo, a clean commit history is paramount for understanding changes across multiple projects.
- Semantic Commit Messages: Adopt a convention like Conventional Commits (e.g.,
feat(mobile-app): Add user profile screen,fix(shared-ui): Resolve button padding issue). This allows for automated changelog generation and easier filtering of history. - Detailed Descriptions: Beyond the subject line, provide a detailed body explaining what was changed, why, and how. This context is invaluable for future debugging and understanding.
feat(auth): Implement JWT token refresh mechanism
- Added new endpoint /auth/refresh for token renewal.
- Implemented secure storage for refresh tokens on client side.
- Updated authentication interceptor to use refresh token automatically.
- Added unit tests for the refresh logic.
4. Efficient Code Review and Collaboration
- Small, Focused Pull Requests (PRs): Break down large features into smaller, manageable PRs. This makes reviews easier, faster, and reduces the chance of introducing bugs.
- Code Owners: Utilize Git platform features like CODEOWNERS files to automatically assign reviewers for specific directories or file types, ensuring that experts review relevant changes.
Submodules: Taming External Dependencies with Care
Git submodules allow you to embed one Git repository inside another. While powerful for managing shared libraries or external dependencies, they come with their own set of considerations. Best practices are crucial to avoid common headaches.
1. Pinning to Specific Commits
Always pin your submodules to a specific commit hash, not a branch. This ensures that your main project always references a known, stable version of the submodule, preventing unexpected changes or breaking builds when the submodule's main branch updates.
# Initialize and update submodules
git submodule update --init --recursive
# Navigate into the submodule and checkout a specific commit
cd path/to/your/submodule
git checkout <specific_commit_hash>
# Go back to the superproject root
cd ../..
# Stage the submodule change and commit it in the superproject
git add path/to/your/submodule
git commit -m "Pin submodule 'my-library' to <specific_commit_hash>"
2. Thoughtful Update Strategies
- Manual Updates: Plan when and how submodules are updated. Often, it's best to update them manually as part of a release cycle or when a new feature explicitly requires an updated version.
- Automated Checks (Optional): For critical submodules, consider a CI job that periodically checks for new stable releases and notifies the team, but avoid automatic updates to your main branch.
3. Avoiding Common Pitfalls
- Detached HEAD State: Submodules often result in a detached HEAD state when checked out. This is normal, as they are pinned to a specific commit. However, avoid making changes directly in a detached HEAD state. If you need to modify a submodule, checkout a branch first, make changes, commit, push, and then update the superproject's reference.
- Circular Dependencies: Ensure your submodules don't create circular dependencies where project A depends on B, and B depends on A. This leads to complex and brittle setups.
- No Local Changes: Avoid making uncommitted local changes within a submodule. These changes won't be tracked by the superproject and can lead to confusion or lost work. Always commit and push changes within the submodule's own repository first.
4. When to Consider Alternatives
While useful, submodules aren't always the best solution. For managing language-specific dependencies (e.g., npm packages, CocoaPods, Maven artifacts), dedicated package managers are almost always a better choice. For sharing code within a single repository without external references, Git subtree can be an option, but it has its own complexities.
Git Workflows: Crafting Collaborative Processes
The workflow you choose dictates how your team collaborates, manages branches, and releases code. A well-defined workflow minimizes conflicts and maximizes productivity.
1. Choosing the Right Workflow
There isn't a one-size-fits-all Git workflow. The best choice depends on your team size, project complexity, and release cadence.
- Trunk-Based Development (TBD): Ideal for fast-paced, continuous integration environments. Developers commit directly to a single
main(ortrunk) branch, using feature flags to control rollouts. Requires high test coverage and frequent, small commits. - GitHub Flow / GitLab Flow: Simple and effective for projects with continuous delivery. Features are developed on short-lived branches, merged into
mainvia pull requests, and then deployed. GitLab Flow adds environment-specific branches (e.g.,pre-production,production) for more controlled releases. - Gitflow: More structured, with dedicated branches for features, releases, and hotfixes. Suitable for projects with strict release cycles and versioning. Can be overkill for smaller teams or projects with rapid deployment needs.
2. Mastering Branching Strategies
- Short-Lived Feature Branches: Regardless of the workflow, keep feature branches as short-lived as possible. Merge them back into
mainfrequently to avoid merge conflicts and allow for continuous integration. - Descriptive Branch Names: Use clear, concise, and descriptive names for your branches. Include prefixes like
feature/,bugfix/,hotfix/, orrelease/.
git checkout -b feature/implement-dark-mode
git checkout -b bugfix/login-crash-on-ios17
3. The Art of the Pull Request (PR)
Pull Requests (or Merge Requests) are central to most modern Git workflows. They are opportunities for code review and collaboration.
- Small, Focused PRs: As mentioned for monorepos, smaller PRs are easier to review and merge. Aim for PRs that address a single logical change or feature.
- Comprehensive Descriptions: Provide context in your PR description: What problem does this solve? How was it implemented? What are the potential impacts? Include screenshots or videos for UI changes.
- Timely Reviews: Foster a culture of prompt code reviews. Unreviewed PRs become stale quickly, leading to more complex merges.
4. Cleaning Up History with Rebase and Squash
Before merging a feature branch, consider cleaning up its history to create a linear, easy-to-read commit log.
- Interactive Rebase (
git rebase -i): Use interactive rebase to combine multiple small, in-progress commits into fewer, more meaningful ones. You can squash, reword, or even drop commits.
# Rebase the last 5 commits interactively
git rebase -i HEAD~5
- Squash Merge: Many Git platforms offer a "squash merge" option, which combines all commits from a feature branch into a single commit when merging into
main. This keeps themainbranch history clean and linear, while still preserving the detailed history on the feature branch.
# Example of a manual squash merge
git checkout main
git merge --squash feature/my-feature-branch
git commit -m "feat: Implement user profile management and settings"
5. Tagging for Releases
Use Git tags to mark specific points in history as important, typically for releases. This provides a stable reference to a particular version of your code.
# Create an annotated tag for a release
git tag -a v1.0.0 -m "Release version 1.0.0 - Initial public launch"
Always push tags to the remote repository: git push origin --tags.
Conclusion: Your Path to Git Mastery
Adopting these best practices for monorepos, submodules, and Git workflows will significantly improve your team's efficiency, reduce errors, and foster a healthier development environment. It's about building habits that support maintainable codebases and scalable collaboration.
At CoddyKit, we believe that mastering advanced Git techniques is a cornerstone of modern software development. By implementing these tips, you're not just using Git; you're leveraging its full power to build better mobile applications.
Next Steps with CoddyKit
Ready to put these best practices into action? Head over to CoddyKit's interactive labs to practice advanced Git commands and scenarios in a real-world environment. Our hands-on courses will guide you through setting up monorepos, managing submodules, and applying various workflows effectively.
Stay tuned for Post 3 in our 'Git Advanced' series, where we'll explore common mistakes and how to avoid them!