{"id":15489137,"url":"https://github.com/hanoii/git-workflow","last_synced_at":"2025-10-28T15:41:43.624Z","repository":{"id":197040694,"uuid":"697836438","full_name":"hanoii/git-workflow","owner":"hanoii","description":"My personal git workflow and collection of useful articles.","archived":false,"fork":false,"pushed_at":"2024-11-21T18:17:10.000Z","size":58,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-27T21:38:54.409Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hanoii.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-28T15:16:00.000Z","updated_at":"2024-11-21T18:17:14.000Z","dependencies_parsed_at":"2024-11-11T22:30:08.967Z","dependency_job_id":"5a893757-f3ea-4d51-9340-833959d52c0e","html_url":"https://github.com/hanoii/git-workflow","commit_stats":null,"previous_names":["hanoii/git-workflow"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hanoii/git-workflow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanoii%2Fgit-workflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanoii%2Fgit-workflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanoii%2Fgit-workflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanoii%2Fgit-workflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hanoii","download_url":"https://codeload.github.com/hanoii/git-workflow/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hanoii%2Fgit-workflow/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259642249,"owners_count":22888982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-02T07:03:55.625Z","updated_at":"2025-10-28T15:41:43.616Z","avatar_url":"https://github.com/hanoii.png","language":"Shell","readme":"# A Git Workflow\n\nThe goal of this workflow is to have a cleaner history, which makes things\neasier to review when you go back in time. In general, there's nothing too\ncomplex, but it includes rebasing, which if not done right and with care can\ncause loss of work. It's gotten a lot better and there are ways to still recover\nthe work, so I am less worried, but the warning is still there.\n\nOne way of explaining rebases is thinking of them as `.patch` files. Imagine\neach of the commits you are rebasing is a `.patch` file and you apply that patch\nfile one after the other on top of a new codebase. Each patch will then create a\nnew commit.\n\nNormally, the `.patch` applies cleanly, but as you have probably encountered, it\nsometimes fails to apply properly. This is when you will put on your surgeon's\ncap and work carefully. It's pretty much the same as sorting out merge\nconflicts, but a merge commit is more easily revertible. The other problem with\nrebases is that they might require you to force-push, which is again dangerous,\nbut in this workflow you would only be force-pushing to the feature branch you\nare working on and about to merge, so it's not a huge deal.\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e All the git configurations mentioned in this workflow\n\u003e are available in [`example.gitconfig`](example.gitconfig). Copy the settings\n\u003e to your `~/.gitconfig` or run the equivalent `git config --global` commands.\n\n\u003c!-- toc --\u003e\n\n- [Pull rebase keeping merges](#pull-rebase-keeping-merges)\n- [Production/Staging branches](#productionstaging-branches)\n- [Pull requests/feature branches](#pull-requestsfeature-branches)\n  * [`--force-with-lease` and `--force-if-includes`](#--force-with-lease-and---force-if-includes)\n- [Rebasing](#rebasing)\n- [Pull request review](#pull-request-review)\n- [Hotfixes to production](#hotfixes-to-production)\n- [Optional squashing](#optional-squashing)\n- [Commit messages](#commit-messages)\n\n\u003c!-- tocstop --\u003e\n\n## Pull rebase keeping merges\n\nTL;DR\n\n- `git pull --rebase-merges`\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e You can configure this as your default pull behavior with\n\u003e `git config --global pull.rebase merges`\n\nThis will fetch whatever is in the remote and reapply your local commits on top\nof the new code. This eliminates unnecessary remote merge commits.\n\nThe `merges` option keeps your local merge commits, if any. This prevents\naccidentally dropping those on the `main`/`staging` branches.\n\nBecause this is just a rebase of your local commits, no force push is necessary.\n\nConflicts can happen, so you can either fix them, commit, and continue the\nrebase (`git rebase --continue`) or abort it (`git rebase --abort`) and go back\nto pulling normally if you want to be cautious: `git pull --merge`.\n\n## Production/Staging branches\n\n**Staging should at all times be deployable to Production.**\n\nTL;DR\n\n- `git checkout main`\n- `git pull`\n- `git merge staging --ff-only`\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e You can configure this globally with\n\u003e `git config --global branch.main.mergeOptions --ff-only` (sets the default merge\n\u003e strategy for the `main` branch).\n\nA common scheme is having at least one production branch (`main`) and a staging\nbranch (`stage`, `staging`, `develop`) that's always where code lands before\nbeing merged onto the production branch.\n\nSince the history between staging and production should ideally always be the\nsame, having a merge commit on `main` from `staging` makes no sense. To avoid\nthis, you should normally merge with `--ff-only`, which performs a fast-forward.\nIf this fails, it's because the history of the production branch has diverged\nand needs to be fixed accordingly.\n\n## Pull requests/feature branches\n\n**Assumes PR/feature branches are created from a staging branch named\n`staging`.**\n\n**PR/feature branches are short-lived; they must be removed once the work is\nmerged.**\n\nTL;DR - when you are ready to merge the PR/feature branch\n\n- `git checkout staging`\n- `git pull`\n- `git checkout feature/branch`\n- `git rebase staging` (can cause conflicts which you'll need to fix)\n- `git push --force-with-lease --force-if-includes`\n- `git checkout staging`\n- `git merge --no-ff feature/branch`\n- `git push`\n- `git push origin :feature/branch` (removes remote branch)\n- `git branch -d feature/branch` (removes local branch)\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e You can configure safer force-pushing as the default with\n\u003e `git config --global push.useForceIfIncludes true` (automatically adds\n\u003e `--force-if-includes` when `--force-with-lease` is used).\n\u003e\n\u003e Note: there's currently no configuration to make `--force-with-lease` the\n\u003e default for pushes.\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e You can also configure no-fast-forward merges as the default with\n\u003e `git config --global merge.ff false` (sets `--no-ff` as the default merge\n\u003e strategy for all branches).\n\u003e\n\u003e Note: we've already configured `main` to use `--ff-only` when merging into it,\n\u003e which is more restrictive and takes precedence for that specific branch.\n\nIf we merge PR/feature branches as-is, multiple PR/feature branches can have\ncommits happening at different times. While this is acceptable, it gives a much\nclearer history graph if we rebase first. Furthermore, rebasing removes the\nmerge commits that we might have accumulated while keeping the feature branch up\nto date, which are not important for the final history.\n\nThis should be done as the last step just before merging the branch into\nstaging, which is especially important if the feature branch is being worked on\nby multiple developers.\n\nYou should push your rebased code to the remote PR/feature branch with\n`git push --force-with-lease --force-if-includes` just before merging.\n\nYou can then check out your staging branch and merge your PR/feature branch with\n`git merge --no-ff feature/branch`. The `--no-ff` flag creates a merge commit\nfor the PR/feature branch so that the history remains accessible.\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!NOTE]\n\u003e PR/feature branches should be short-lived, so make sure you remove both\n\u003e the remote (`git push origin :feature/branch`) and local PR/feature branch\n\u003e (`git branch -d feature/branch`).**\n\n### `--force-with-lease` and `--force-if-includes`\n\nIt's important to use `--force-with-lease` together with `--force-if-includes`\nwhen force-pushing rebased branches.\n\n`--force-with-lease` alone can be defeated by background auto-fetches (common in\nIDEs like LazyGit, VS Code, etc.) that update your remote-tracking branch\nwithout you realizing it. When this happens, `--force-with-lease` thinks you've\nseen the latest remote changes and allows the force-push, potentially\noverwriting others' work.\n\n`--force-if-includes` adds an extra safety check: it uses your reflog to verify\nyou've actually integrated remote changes into your local branch before allowing\nthe force-push.\n\n**References:**\n\n- https://github.com/jesseduffield/lazygit/issues/1668#issuecomment-1956201168\n- https://github.com/jesseduffield/lazygit/issues/1668#issuecomment-1956549518\n- https://stackoverflow.com/questions/65837109/when-should-i-use-git-push-force-if-includes\n\n## Rebasing\n\nWhen rebasing branches that contain merge commits (such as rebasing `staging`\nitself), use `--rebase-merges` to preserve the existing merge commits from\nfeature branches. This is a safer rebasing approach that maintains the merge\nstructure rather than linearizing all commits.\n\n```bash\ngit rebase --rebase-merges main\n```\n\nThis is particularly useful when you need to rebase a staging branch that\ncontains multiple feature branch merges and you want to preserve that merge\nhistory in the rebased result.\n\nNote: You can use `--no-rebase-merges` to explicitly disable this behavior if\nneeded.\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e You can configure this as the default rebase behavior with\n\u003e `git config --global rebase.rebaseMerges true` (sets `--rebase-merges` as\n\u003e the default for all rebase operations).\n\n## Pull request review\n\nHere's a useful emoji code you can use for verbose code reviews:\nhttps://gist.github.com/pfleidi/4422a5cac5b04550f714f1f886d2feea\n\n## Hotfixes to production\n\nTo be documented. The most important element is keeping `main`/`staging` with\nexactly the same history.\n\n## Optional squashing\n\nI am not a fan of squashing, but when used with common sense it can be helpful.\nIf your feature branch is full of small commits that touch very few files/lines,\nit makes more sense to squash them than to merge the whole history.\n\nTL;DR\n\n- `git checkout staging`\n- `git merge feature/branch --squash` (there's no commit here yet, but changes\n  are staged)\n- `git commit -m \"JIRA-1234: something done\"` (JIRA-1234 or whatever references\n  your PM tool of choice)\n- `git push`\n- `git push origin :feature/branch` (removes remote branch)\n- `git branch -d feature/branch` (removes local branch)\n\n## Commit messages\n\nText from https://github.blog/2011-09-06-shiny-new-commit-styles/:\n\n**Always include a reference to the task (Jira, Trello, ClickUp) in the summary\nor the description.**\n\n**If at all possible, look for integration between the PM tool and the commit.**\n\n```\nCapitalized, short (50 chars or less) summary\n\nMore detailed explanatory text, if necessary. Wrap it to about 72 characters or\nso. In some contexts, the first line is treated as the subject of an email and\nthe rest of the text as the body. The blank line separating the summary from the\nbody is critical (unless you omit the body entirely); tools like rebase can get\nconfused if you run the two together.\n\nWrite your commit message in the present tense: \"Fix bug\" and not \"Fixed bug.\"\nThis convention matches up with commit messages generated by commands like `git\nmerge` and `git revert`.\n\nFurther paragraphs come after blank lines.\n\n- Bullet points are okay, too\n- Typically a hyphen or asterisk is used for the bullet, preceded by a single\n  space, with blank lines in between, but conventions vary here\n- Use a hanging indent\n```\n\n\u003c!-- prettier-ignore --\u003e\n\u003e [!TIP]\n\u003e I find **_50 chars or less_** too short. I normally use the\n\u003e [amazing GitSavvy Sublime Text plugin](https://github.com/timbrel/GitSavvy)\n\u003e that has a\n\u003e [sensible warning at +20 characters](https://github.com/timbrel/GitSavvy/blob/f2e6abd619558934de59bab9ebd0d750476798da/GitSavvy.sublime-settings#L134)\n\u003e making **70 characters** a good summary line length.\n\nFurthermore, using [conventional commits][cc] can create a very nice changelog\nfrom commit messages and also encourages you to [scope your commits\nbetter][cc-scope].\n\n```\n\u003ctype\u003e[optional scope]: \u003cdescription\u003e\n\n[optional body]\n\n[optional footer(s)]\n```\n\nFrom the [Conventional Commits specification][cc]:\n\n\u003e The commit contains the following structural elements, to communicate intent\n\u003e to the consumers of your library:\n\u003e\n\u003e - **fix:** a commit of the type `fix` patches a bug in your codebase (this\n\u003e   correlates with PATCH in Semantic Versioning).\n\u003e - **feat:** a commit of the type `feat` introduces a new feature to the\n\u003e   codebase (this correlates with MINOR in Semantic Versioning).\n\u003e - **BREAKING CHANGE**: a commit that has a footer `BREAKING CHANGE:`, or\n\u003e   appends a `!` after the type/scope, introduces a breaking API change\n\u003e   (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be\n\u003e   part of commits of any type.\n\u003e - _types_ other than `fix:` and `feat:` are allowed, for example\n\u003e   [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)\n\u003e   (based on the\n\u003e   [Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines))\n\u003e   recommends `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`,\n\u003e   `perf:`, `test:`, and others.\n\u003e - _footers_ other than `BREAKING CHANGE: \u003cdescription\u003e` may be provided and\n\u003e   follow a convention similar to\n\u003e   [git trailer format](https://git-scm.com/docs/git-interpret-trailers).\n\u003e\n\u003e Additional types are not mandated by the Conventional Commits specification,\n\u003e and have no implicit effect in Semantic Versioning (unless they include a\n\u003e BREAKING CHANGE). A scope may be provided to a commit's type, to provide\n\u003e additional contextual information and is contained within parentheses, e.g.,\n\u003e `feat(parser): add ability to parse arrays`.\n\n[cc]: https://www.conventionalcommits.org/\n[cc-scope]:\n  https://www.conventionalcommits.org/en/v1.0.0/#what-do-i-do-if-the-commit-conforms-to-more-than-one-of-the-commit-types\n\nOther useful readings:\n\n- https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html\n- https://markus.oberlehner.net/blog/git-the-pedantic-way/\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanoii%2Fgit-workflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhanoii%2Fgit-workflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanoii%2Fgit-workflow/lists"}