5.6 KiB
Contributing to topgrade
Thank you for your interest in contributing to topgrade!
We welcome and encourage contributions of all kinds, such as:
- Issue reports or feature requests
- Documentation improvements
- Code (PR or PR Review)
LLM/AI guidelines
You may use LLMs (AI tools) for:
- Inspiration, problem solving, help with Rust, translation, etc.
- Generating small and self-contained snippets of code (e.g., shell scripts or utility functions)
Do not use LLMs to:
- Generate ("vibe code") entire pull requests
- Write or generate issue or pull request descriptions
General guidelines
Please use conventional commits for your PR title.
We use pre-commit. It runs in CI, but you can optionally install the hook
locally with pre-commit install. If you don't want to use pre-commit, make sure the following pass before submitting
your PR:
$ cargo fmt
$ cargo clippy
$ cargo test
Adding a new step
In topgrade's terms, a package manager (or something else that can be upgraded) is called a step.
To add a new step to topgrade:
-
Add a new variant to
enum Steppub enum Step { // Existing steps // ... // Your new step here! // Make sure it stays sorted alphabetically because that looks great :) Xxx, } -
Implement the update function
You need to find the appropriate location where this update function goes, it should be a file under
src/steps, the file names are self-explanatory, for example, steps related tozshare placed insteps/zsh.rs, and steps that run on Linux only are placed insteps/linux.rs.Then you implement the update function, and put it in the file where it belongs.
pub fn run_xxx(ctx: &ExecutionContext) -> Result<()> { // Check if this step is installed, if not, then this update will be skipped. let xxx = require("xxx")?; // Print the separator print_separator("xxx"); // Invoke the new step to get things updated! ctx.execute(xxx) .arg(/* args required by this step */) .status_checked() }Such an update function would be conventionally named
run_xxx(), wherexxxis the name of the new step, and it should take an argument of type&ExecutionContext.The update function should usually do 3 things:
- Check if the step is installed
- Output the separator
- Execute commands
This is sufficient for most tools, but you may need some extra stuff for complicated steps.
-
Add a match arm to
Step::run()Xxx => runner.execute(*self, "xxx", || ItsModule::run_xxx(ctx))?We use conditional compilation to separate the steps. For example, for steps that are Linux-only, it goes like this:
#[cfg(target_os = "linux")] { // Xxx is Linux-only runner.execute(Step::Xxx, "xxx", || ItsModule::run_xxx(&ctx))?; } -
Finally, add the step to
default_steps()instep.rssteps.push(Xxx)Keep the conditional compilation the same as in the above step 3.
Congrats, you just added a new step :)
Modification to the configuration entries
If your PR has the configuration options
(in src/config.rs)
modified:
- Adding new options
- Changing the existing options
Be sure to apply your changes to
config.example.toml,
and have some basic documentations guiding user how to use these options.
Breaking changes
If your PR introduces a breaking change, document it in BREAKINGCHANGES_dev.md.
It should be written in Markdown and wrapped at 80, for example:
1. The configuration location has been updated to x.
2. The step x has been removed.
3. ...
I18n
If your PR introduces user-facing messages, we need to ensure they are translated.
Please add the translations to locales/app.yml. For simple messages
without arguments (e.g., "hello world"), we can simply translate them according
(Tip: LLMs are good at translation). If a message contains
arguments, e.g., "hello ", please follow this convention:
"hello {name}": # key
en: "hello %{name}" # translation
Arguments in the key should be in format {argument_name}, and they will have
a preceding % when used in translations.
Locales
Some steps respect locale, which means their output can be in language other than English. In those cases, we cannot rely on the output of a command.
For example, one may want to check if a tool works by doing this:
let output = Command::new("xxx").arg("--help").output().unwrap();
let stdout = from_utf8(output.stdout).expect("Assume it is UTF-8 encoded");
if stdout.contains("help") {
// xxx works
}
If xxx respects locale, then the above code should work on English system,
on a system that does not use English, e.g., it uses Chinese, that "help" may be
translated to "帮助", and the above code won't work.