Metadata-Version: 2.5
Name: micropython-branch-manager
Version: 3.0.1
Summary: Tool for managing MicroPython fork integration branches
Project-URL: Homepage, https://gitlab.com/alelec/micropython-branch-manager
Project-URL: Repository, https://gitlab.com/alelec/micropython-branch-manager.git
Project-URL: Issues, https://gitlab.com/alelec/micropython-branch-manager/-/issues
Author-email: Andrew Leech <andrew@alelec.net>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cyclopts>=3.0
Requires-Dist: pydantic>=2.0
Requires-Dist: tomli-w>=1.0
Description-Content-Type: text/markdown

# micropython-branch-manager

`mbm` keeps a fork of a GitHub project up to date when you're carrying a stack of unmerged PRs and local feature branches on top of upstream. It was written for MicroPython (hence the name) but works with any GitHub-hosted project.

The idea is simple. Your fork has an _integration branch_ (say `main` or `mimxrt`) which is basically upstream plus one merge commit per feature branch. When upstream moves, `mbm rebase` throws that branch away and rebuilds it: each feature branch is rebased onto the new upstream on its own, then merged in, in the order listed in `mbm.toml`. You get a clean, readable history where every feature is still an isolated branch you can push back to its PR.

On top of that it:

- adds new PRs by number, URL or branch name, creating a remote for the PR author's fork if needed
- skips PRs that GitHub says have already been merged upstream
- pushes rebased feature branches back to their owner's fork (never to upstream), and checks they haven't changed on the remote first
- trains `git rerere` from your previous integration branch, so conflicts you've already resolved get resolved again automatically
- builds the result on a separate `<integration>_update` branch and, if you use GitLab, gives you a pre-filled merge request link to review it

## Install

```bash
uv tool install micropython-branch-manager
```

You'll also want the GitHub CLI (`gh`) installed and logged in (`gh auth login`). It's used for PR lookups; without it `mbm` still works but can't skip merged PRs or fill in PR details.

## Quick start

`mbm` expects your remotes to look like this:

- `upstream`: the GitHub project you're tracking, e.g. `https://github.com/micropython/micropython.git`
- one remote per fork you push to, e.g. `andrewleech` or `origin` pointing at `git@github.com:andrewleech/micropython.git`
- optionally a GitLab remote, if you review integration updates as GitLab MRs

If MicroPython is a submodule of your project, run `init` from the project root. It writes `mbm.toml` there (commit it with the project) and finds the submodule at `src/micropython` or `micropython`:

```bash
cd my-project
mbm init --submodule src/micropython --integration-branch main
```

If you're working directly in a fork clone, point it at the repo itself:

```bash
cd my-fork
mbm init --submodule . --integration-branch main
```

Then add some PRs and rebuild whenever upstream moves:

```bash
mbm add-pr 18333                  # by PR number
mbm add-pr https://github.com/micropython/micropython/pull/18229
mbm rebase                        # rebuild on the latest upstream
```

`add-pr` and `rebase` both leave your integration branch alone and do their work on `<integration>_update`. Check it over (or open the GitLab MR it prints), then either merge that MR or run `mbm rebase --apply` to move the integration branch across once the rebuild is clean.

## Commands

### `mbm init`

Creates `mbm.toml` in the current directory (or the `--config` path) and adds a submodule entry to it. If you don't pass `--integration-branch` it uses whatever branch the repo has checked out. It also turns on `git rerere` in that repo. Running it again with a different `--submodule` adds another entry, so one config can manage several submodules; the other commands pick one with `--submodule`/`-s`, or work it out from your current directory.

### `mbm add-pr <pr>`

Adds a PR to the integration branch as a merge commit. The PR can be a number, a branch name or a full URL (it has to be a PR against the `upstream` repo).

It looks the PR up on GitHub, fetches it, merges it into `<integration>_update` and adds it to `mbm.toml`, including the PR author so later pushes go to the right fork. If a GitLab remote exists the update branch is pushed there and you get an MR link.

```
$ mbm add-pr 18333
Fetching PR info for: 18333
Found PR #18333: ports/mimxrt: Update nxp_driver to MCUX_2.16.100.
Branch: mcux_sdk_2.16
State: OPEN

Creating update branch from mimxrt...
Fetching PR #18333 from upstream...
Merging mcux_sdk_2.16 into mimxrt_update...
Merge completed successfully

Pushing mimxrt_update to gitlab...

=== PR ADDED SUCCESSFULLY ===
Create MR: https://gitlab.example.com/.../merge_requests/new?...
```

Adding a second PR builds on the same update branch, so you can queue up a few before reviewing.

### `mbm rebase`

Rebuilds the integration branch on top of a new upstream:

```bash
mbm rebase                        # onto the default target
mbm rebase --target v1.27.0       # onto a particular ref, e.g. a release tag
```

The target is picked in this order: `--target`, then `target` in `mbm.toml`, then upstream's default branch (`upstream/HEAD`, which `mbm` sets up with `git remote set-head upstream --auto` if it's missing), and finally `upstream/master`.

Rebuilding onto a release tag works too. If the target is older than upstream's default branch, each PR branch (which is usually based on the latest upstream) is rebased with `git rebase --onto <target> <upstream default>`, so only the PR's own commits get moved onto the tag, not everything upstream did since.

Before touching anything it asks GitHub for the state of each PR and skips any that are merged, printing which ones. They stay in `mbm.toml` until you remove them.

Options:

- `--local` skips fetching and pushing, handy for a dry run of conflicts
- `--dry-run` shows what it'd do without changing anything
- `--apply` moves the integration branch to `<integration>_update` once the run is clean (`git checkout -B`). Use this if you don't review through GitLab.
- `--no-update-branches` only builds the update branch; your feature branch refs aren't moved, tracked or pushed. Useful if you've got feature branches checked out in other worktrees or just don't want them touched. `--update-branches` forces the opposite, and the default comes from `update_feature_branches` in the config.
- `--force-push` pushes even if a feature branch on the remote has moved since you last fetched it (careful, you'll lose whatever's there)
- `--resume` carries on after you've fixed a conflict (see below)

Here's what a rebuilt branch looks like, every feature is its own branch off the target, merged in order:

```
*   c1c523ebd7 - Merge branch 'mimxrt1176-alt11-pwm' (HEAD -> mimxrt_update)
|\
| * e16d226353 - mimxrt: Add ALT11 pin mode support for MIMXRT1176. (mimxrt1176-alt11-pwm)
* |   a288f37e95 - Merge branch 'adc'
|\ \
| * | 30aa89db5e - mimxrt/machine_adc: rt117x: Support channel groups. (adc)
| * | b045f8ae4f - mimxrt/machine_adc: rt117x: Initialize LPADC2.
* | |   6f84252a13 - Merge branch 'mimx_sdcard_timeouts'
|\ \ \
| * | | 623409093a - mimxrt/sdcard: Improve robustness of sdcard driver. (mimx_sdcard_timeouts)
| * | | 5dd59d4e07 - mimxrt/sdcard: Fix deadlock in sdcard_power_off.
* | | |   62c3ccf986 - Merge branch 'mimx_Flash_doc'
|\ \ \ \
| * | | | cad3bd124a - docs/mimxrt: Add docs for mimxrt.Flash. (mimx_Flash_doc)
| |/ / /
* | | |   864c580cfa - Merge branch 'dp83867-phy-driver'
|\ \ \ \
| * | | | 76fcf3ce95 - mimxrt/eth: Improve Dual Ethernet configuration. (dp83867-phy-driver)
| * | | | 7ad3bbaff7 - mimxrt/boards/MIMXRT1170_EVK: Remove obsolete pin defines.
| * | | | 6a70a07795 - mimxrt/eth: Add DP83867 PHY driver support.
| |/ / /
* | | |   03163eaaeb - Merge branch 'phyboard-rt1170'
|\ \ \ \
| * | | | 99d763bffb - mimxrt: Add PHYBOARD-RT1170 board support. (phyboard-rt1170)
| |/ / /
* | | |   345a5419a3 - Merge branch 'manifest_c_module'
|\ \ \ \
| * | | | 12b45387e5 - tools/ci: Add c_module() testing for RP2 and STM32. (manifest_c_module)
| * | | | ... (more commits)
* | | | |   b61786d615 - Merge branch 'mcux_sdk_2.16'
|\ \ \ \ \
| * | | | | 66be1ee6a8 - mimxrt/fsl_lpuart: Use wrapper for IRQ Idle support. (mcux_sdk_2.16)
| * | | | | 8c34a2df96 - ports/mimxrt: Update nxp_driver to MCUX_2.16.100.
|/ / / / /
* / / / / 78ff170de9 - all: Bump version to 1.27.0. (upstream/master, mimxrt)
```

### `mbm sync <github-user>`

Brings `mbm.toml` back in line with what's actually on the integration branch. It reads the merge commits, adds any branches that are missing, and fills in PR number, URL, title and author by looking up PRs from `<github-user>` (plus any branch it can find a PR for). It's the easy way to start using `mbm` on a fork you've been maintaining by hand, or to backfill `author` in an older config.

If a PR in the config has been merged upstream, `sync` tells you but leaves the entry alone; take it out yourself when you're ready (sometimes you want to keep it pinned for a while).

### `mbm config`

Prints the integration branch, configured branches and remotes.

## Configuration

`mbm.toml` is written by `init`, `add-pr` and `sync`, but it's plain TOML and fine to edit by hand. Branch order matters, it's the merge order.

```toml
[[submodules]]
path = "src/micropython"
integration_branch = "main"
# target = "v1.27.0"
# update_feature_branches = false

[[submodules.branches]]
name = "mcux_sdk_2.16"
pr_url = "https://github.com/micropython/micropython/pull/18333"
pr_number = 18333
title = "ports/mimxrt: Update nxp_driver to MCUX_2.16.100."
author = "andrewleech"

[[submodules.branches]]
name = "my-local-hack"   # no PR, just a local branch
```

Per submodule:

- `path`: the repo, relative to `mbm.toml`
- `integration_branch`: the branch `mbm` rebuilds
- `target`: default rebase target, if you don't want upstream's default branch
- `update_feature_branches`: set to `false` to leave feature branch refs alone during `rebase` (default `true`)

Per branch:

- `name`: local branch name, the only required field
- `pr_url`, `pr_number`, `title`: the PR it belongs to, if any
- `author`: GitHub owner of the fork the branch lives in, used to decide where to push it

Older configs with everything at the top level (no `[[submodules]]`) still load fine.

The GitHub repo used for PR lookups comes from the `upstream` remote. If there isn't a GitHub `upstream` remote it assumes `micropython/micropython`.

## Pushing

With a normal (non `--local`) run, `rebase` pushes:

- each feature branch to its owner's fork. That's the remote whose URL matches the branch's `author`; if `author` isn't set yet it's taken from the PR on GitHub, or from `pr_url` when that points at a fork. It'll never push to `upstream` or any other remote for the upstream repo. No matching remote means the branch is skipped with a warning.
- the update branch to your GitLab remote, if you have one, along with an MR link that has the title and description filled in:

__omp_shell("[GitLab MR Example](docs/gitlab_mr_example.png)")

Before force-pushing a feature branch it checks whether the remote copy has commits you don't have (someone else pushed, or you pushed from another machine). If so that branch is skipped and reported at the end, the rest still go.

## Conflicts

If a rebase hits a conflict `rerere` can't fix, `mbm` stops, tells you which PR and files, and saves its progress to `.git/mbm-rebase-state.json`:

```
Rebase stopped due to conflicts while integrating PR #12345 (feature-branch).
Conflicting files:
  ports/stm32/main.c
  py/compile.c

Please resolve conflicts manually, then run:
  cd /path/to/micropython
  git rebase --continue

Then resume the integration:
  mbm rebase --resume
```

Fix it, `git rebase --continue`, then `mbm rebase --resume` and it picks up where it left off. `rerere` remembers your fix, so next time it'll be applied automatically.

## Development

```bash
git clone https://gitlab.com/alelec/micropython-branch-manager.git
cd micropython-branch-manager
uv sync
uv run pytest
uv run pre-commit install        # ruff + mypy on commit
uv run pre-commit run --all-files
```

Versions come from git tags via `hatch-vcs`. Pushing a tag like `v2.2.0` gets CI to publish it to PyPI; in between you'll see dev versions like `2.1.2.dev3+g1b5fe36`.

## License

MIT
