Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: Use josh for subtree syncs #17025

Merged
merged 1 commit into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rust-analyzer/tests/slow-tests/tidy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ MIT OR Apache-2.0
MIT OR Apache-2.0 OR Zlib
MIT OR Zlib OR Apache-2.0
MIT/Apache-2.0
MPL-2.0
Unlicense OR MIT
Unlicense/MIT
Zlib OR Apache-2.0 OR MIT
Expand Down
20 changes: 15 additions & 5 deletions docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,22 @@ Release steps:
* publishes the VS Code extension to the marketplace
* call the GitHub API for PR details
* create a new changelog in `rust-analyzer.github.io`
3. While the release is in progress, fill in the changelog
4. Commit & push the changelog
3. While the release is in progress, fill in the changelog.
4. Commit & push the changelog.
5. Run `cargo xtask publish-release-notes <CHANGELOG>` -- this will convert the changelog entry in AsciiDoc to Markdown and update the body of GitHub Releases entry.
6. Tweet
7. Inside `rust-analyzer`, run `cargo xtask promote` -- this will create a PR to rust-lang/rust updating rust-analyzer's subtree.
Self-approve the PR.
6. Tweet.
7. Make a new branch and run `cargo xtask rustc-pull`, open a PR, and merge it.
This will pull any changes from `rust-lang/rust` into `rust-analyzer`.
8. Switch to `master`, pull, then run `cargo xtask rustc-push --rust-path ../rust-rust-analyzer --rust-fork matklad/rust`.
Replace `matklad/rust` with your own fork of `rust-lang/rust`.
You can use the token to authenticate when you get prompted for a password, since `josh` will push over HTTPS, not SSH.
This will push the `rust-analyzer` changes to your fork.
You can then open a PR against `rust-lang/rust`.

Note: besides the `rust-rust-analyzer` clone, the Josh cache (stored under `~/.cache/rust-analyzer-josh`) will contain a bare clone of `rust-lang/rust`.
This currently takes about 3.5 GB.

This [HackMD](https://hackmd.io/7pOuxnkdQDaL1Y1FQr65xg) has details about how `josh` syncs work.

If the GitHub Actions release fails because of a transient problem like a timeout, you can re-run the job from the Actions console.
If it fails because of something that needs to be fixed, remove the release tag (if needed), fix the problem, then start over.
Expand Down
1 change: 1 addition & 0 deletions rust-version
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you create this file by hand? I would advise against that. This branch hasn't actually had a rustc-pull with the given commit. The file ideally is only ever written by rustc-pull.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was created by hand, but stale. I replaced it with an empty one.

It still needs to be there for the first rustc-pull, otherwise git commit will fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustc-pull should create the file, so creating an empty one should not be needed.

In Miri this happens here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will, but it needs a git add. git commit rust-version won't add it when it's new.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, damn... that's unfortunate.

Copy link
Member

@RalfJung RalfJung Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to make sure, you don't do the pulls and pushes in pairs for miri?

No, we do not. Pulls happen basically automatically: every morning a CI workflow tries to build and test Miri against the latest rustc HEAD, and if that fails it does a rustc-pull and creates a PR for that. (It is rare for Miri to change in rustc without some related internal rustc change that would make our CI fail.) Pushes I do manually every weekend.

It is definitely fine to rustc-pull as often as you want.

In the beginning I was worried about pushing twice without an intermediate pull. But josh seems to be deterministic -- when you do that, the first push will be perfectly re-constructed as part of the second push, so no duplicate commits are created. That said, we do pulls a lot more often than pushes, so we rarely have double-push without intermediate pull, so it's possible that under rare circumstances, this causes issues. I don't think I have seen that ever since we introduced the rust-version file to keep track of which commit to base the push on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks. We'd normally do a push every week (on every release). Pulls are less important, since there's maybe one "downstream" change every 4-6 weeks.

But we did these in pairs, even when pulling didn't bring any changes, because git-subtree can get a little confused otherwise.

How do you avoid empty pulls? Looking at https://github.com/rust-lang/miri/blob/master/.github/workflows/ci.yml#L193, I don't see anything that prevents them, but the job sometimes shows up as skipped.

Copy link
Member

@RalfJung RalfJung Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job is skipped when nightly CI succeeds.

Got it, thanks. We'd normally do a push every week (on every release). Pulls are less important, since there's maybe one "downstream" change every 4-6 weeks.

You're not directly using rustc internal APIs, I assume? Yeah then things look quite differently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, that explains it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When doing a push without intermediate pull, it might be worth checking the commits that the github PR lists just to make sure there are no duplicates with the previous push. At least the first few times, until you are confident the tool works correctly. :)

Also, upgrading josh can sometimes mean the algorithm changes a bit. So it may be worth doing a "safety pull" when changing josh versions. They don't release very often though so that's not going to be coming up very much.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
688c30dc9f8434d63bddb65bd6a4d2258d19717c
1 change: 1 addition & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ rust-version.workspace = true

[dependencies]
anyhow.workspace = true
directories = "5.0"
flate2 = "1.0.24"
write-json = "0.1.2"
xshell.workspace = true
Expand Down
38 changes: 29 additions & 9 deletions xtask/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ xflags::xflags! {
cmd install {
/// Install only VS Code plugin.
optional --client
/// One of 'code', 'code-exploration', 'code-insiders', 'codium', or 'code-oss'.
/// One of `code`, `code-exploration`, `code-insiders`, `codium`, or `code-oss`.
optional --code-bin name: String

/// Install only the language server.
optional --server
/// Use mimalloc allocator for server
/// Use mimalloc allocator for server.
optional --mimalloc
/// Use jemalloc allocator for server
/// Use jemalloc allocator for server.
optional --jemalloc
/// build in release with debug info set to 2
/// build in release with debug info set to 2.
optional --dev-rel
}

Expand All @@ -32,9 +32,21 @@ xflags::xflags! {
cmd release {
optional --dry-run
}
cmd promote {
optional --dry-run

cmd rustc-pull {
/// rustc commit to pull.
optional --commit refspec: String
}

cmd rustc-push {
/// rust local path, e.g. `../rust-rust-analyzer`.
required --rust-path rust_path: String
/// rust fork name, e.g. `matklad/rust`.
required --rust-fork rust_fork: String
/// branch name.
optional --branch branch: String
}

cmd dist {
/// Use mimalloc allocator for server
optional --mimalloc
Expand Down Expand Up @@ -77,7 +89,8 @@ pub enum XtaskCmd {
Install(Install),
FuzzTests(FuzzTests),
Release(Release),
Promote(Promote),
RustcPull(RustcPull),
RustcPush(RustcPush),
Dist(Dist),
PublishReleaseNotes(PublishReleaseNotes),
Metrics(Metrics),
Expand All @@ -104,8 +117,15 @@ pub struct Release {
}

#[derive(Debug)]
pub struct Promote {
pub dry_run: bool,
pub struct RustcPull {
pub commit: Option<String>,
}

#[derive(Debug)]
pub struct RustcPush {
pub rust_path: String,
pub rust_fork: String,
pub branch: Option<String>,
}

#[derive(Debug)]
Expand Down
3 changes: 2 additions & 1 deletion xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ fn main() -> anyhow::Result<()> {
flags::XtaskCmd::Install(cmd) => cmd.run(sh),
flags::XtaskCmd::FuzzTests(_) => run_fuzzer(sh),
flags::XtaskCmd::Release(cmd) => cmd.run(sh),
flags::XtaskCmd::Promote(cmd) => cmd.run(sh),
flags::XtaskCmd::RustcPull(cmd) => cmd.run(sh),
flags::XtaskCmd::RustcPush(cmd) => cmd.run(sh),
flags::XtaskCmd::Dist(cmd) => cmd.run(sh),
flags::XtaskCmd::PublishReleaseNotes(cmd) => cmd.run(sh),
flags::XtaskCmd::Metrics(cmd) => cmd.run(sh),
Expand Down
165 changes: 150 additions & 15 deletions xtask/src/release.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
mod changelog;

use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

use anyhow::{bail, Context as _};
use directories::ProjectDirs;
use stdx::JodChild;
use xshell::{cmd, Shell};

use crate::{codegen, date_iso, flags, is_release_tag, project_root};
Expand Down Expand Up @@ -71,26 +78,154 @@ impl flags::Release {
}
}

impl flags::Promote {
// git sync implementation adapted from https://github.com/rust-lang/miri/blob/62039ac/miri-script/src/commands.rs
impl flags::RustcPull {
pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> {
let _dir = sh.push_dir("../rust-rust-analyzer");
cmd!(sh, "git switch master").run()?;
cmd!(sh, "git fetch upstream").run()?;
cmd!(sh, "git reset --hard upstream/master").run()?;
sh.change_dir(project_root());
let commit = self.commit.map(Result::Ok).unwrap_or_else(|| {
let rust_repo_head =
cmd!(sh, "git ls-remote https://github.com/rust-lang/rust/ HEAD").read()?;
rust_repo_head
.split_whitespace()
.next()
.map(|front| front.trim().to_owned())
.ok_or_else(|| anyhow::format_err!("Could not obtain Rust repo HEAD from remote."))
})?;
// Make sure the repo is clean.
if !cmd!(sh, "git status --untracked-files=no --porcelain").read()?.is_empty() {
bail!("working directory must be clean before running `cargo xtask pull`");
}
// Make sure josh is running.
let josh = start_josh()?;

let date = date_iso(sh)?;
let branch = format!("rust-analyzer-{date}");
cmd!(sh, "git switch -c {branch}").run()?;
cmd!(sh, "git subtree pull -m ':arrow_up: rust-analyzer' -P src/tools/rust-analyzer rust-analyzer release").run()?;
// Update rust-version file. As a separate commit, since making it part of
// the merge has confused the heck out of josh in the past.
// We pass `--no-verify` to avoid running any git hooks that might exist,
// in case they dirty the repository.
sh.write_file("rust-version", format!("{commit}\n"))?;
const PREPARING_COMMIT_MESSAGE: &str = "Preparing for merge from rust-lang/rust";
cmd!(sh, "git commit rust-version --no-verify -m {PREPARING_COMMIT_MESSAGE}")
.run()
.context("FAILED to commit rust-version file, something went wrong")?;

if !self.dry_run {
cmd!(sh, "git push -u origin {branch}").run()?;
cmd!(
sh,
"xdg-open https://github.com/matklad/rust/pull/new/{branch}?body=r%3F%20%40ghost"
)
// Fetch given rustc commit.
cmd!(sh, "git fetch http://localhost:{JOSH_PORT}/rust-lang/rust.git@{commit}{JOSH_FILTER}.git")
.run()
.map_err(|e| {
// Try to un-do the previous `git commit`, to leave the repo in the state we found it it.
cmd!(sh, "git reset --hard HEAD^")
.run()
.expect("FAILED to clean up again after failed `git fetch`, sorry for that");
e
})
.context("FAILED to fetch new commits, something went wrong (committing the rust-version file has been undone)")?;

// Merge the fetched commit.
const MERGE_COMMIT_MESSAGE: &str = "Merge from rust-lang/rust";
cmd!(sh, "git merge FETCH_HEAD --no-verify --no-ff -m {MERGE_COMMIT_MESSAGE}")
.run()
.context("FAILED to merge new commits, something went wrong")?;

drop(josh);
Ok(())
}
}

impl flags::RustcPush {
pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> {
let branch = self.branch.as_deref().unwrap_or("sync-from-ra");
let rust_path = self.rust_path;
let rust_fork = self.rust_fork;

sh.change_dir(project_root());
let base = sh.read_file("rust-version")?.trim().to_owned();
// Make sure the repo is clean.
if !cmd!(sh, "git status --untracked-files=no --porcelain").read()?.is_empty() {
bail!("working directory must be clean before running `cargo xtask push`");
}
// Make sure josh is running.
let josh = start_josh()?;

// Find a repo we can do our preparation in.
sh.change_dir(rust_path);

// Prepare the branch. Pushing works much better if we use as base exactly
// the commit that we pulled from last time, so we use the `rust-version`
// file to find out which commit that would be.
println!("Preparing {rust_fork} (base: {base})...");
if cmd!(sh, "git fetch https://github.com/{rust_fork} {branch}")
.ignore_stderr()
.read()
.is_ok()
{
bail!(
"The branch `{branch}` seems to already exist in `https://github.com/{rust_fork}`. Please delete it and try again."
);
}
cmd!(sh, "git fetch https://github.com/rust-lang/rust {base}").run()?;
cmd!(sh, "git push https://github.com/{rust_fork} {base}:refs/heads/{branch}")
.ignore_stdout()
.ignore_stderr() // silence the "create GitHub PR" message
.run()?;
println!();

// Do the actual push.
sh.change_dir(project_root());
println!("Pushing rust-analyzer changes...");
cmd!(
sh,
"git push http://localhost:{JOSH_PORT}/{rust_fork}.git{JOSH_FILTER}.git HEAD:{branch}"
)
.run()?;
println!();

// Do a round-trip check to make sure the push worked as expected.
cmd!(
sh,
"git fetch http://localhost:{JOSH_PORT}/{rust_fork}.git{JOSH_FILTER}.git {branch}"
)
.ignore_stderr()
.read()?;
let head = cmd!(sh, "git rev-parse HEAD").read()?;
let fetch_head = cmd!(sh, "git rev-parse FETCH_HEAD").read()?;
if head != fetch_head {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this is pretty crucial. If you continue and merge this into rustc despite this check failing, the RA repo may need a force-push to fix the situation. That's what happened in Miri.

I haven't seen this check fail in over a year.

Copy link
Member Author

@lnicola lnicola Apr 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to do it:

$ git fetch https://github.com/rust-lang/rust 4e431fad67b46c480f1833119cd368fa33df95f7
remote: Enumerating objects: 161, done.
remote: Counting objects: 100% (120/120), done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 161 (delta 107), reused 109 (delta 105), pack-reused 41
Receiving objects: 100% (161/161), 72.28 KiB | 7.23 MiB/s, done.
Resolving deltas: 100% (108/108), completed with 76 local objects.
From https://github.com/rust-lang/rust
 * branch                    4e431fad67b46c480f1833119cd368fa33df95f7 -> FETCH_HEAD
$ git push https://github.com/lnicola/rust 4e431fad67b46c480f1833119cd368fa33df95f7:refs/heads/sync-from-ra

Pushing rust-analyzer changes...
$ git push http://localhost:42042/lnicola/rust.git:rev(f5a9250147f6569d8d89334dc9cca79c0322729f:prefix=src/tools/rust-analyzer):/src/tools/rust-analyzer.git HEAD:sync-from-ra
Enumerating objects: 1335, done.
Counting objects: 100% (851/851), done.
Delta compression using up to 32 threads
Compressing objects: 100% (293/293), done.
Writing objects: 100% (500/500), 97.33 KiB | 48.67 MiB/s, done.
Total 500 (delta 332), reused 351 (delta 194), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (332/332), completed with 143 local objects.
remote: josh-proxy
remote: response from upstream:
remote: To https://github.com/lnicola/rust.git
remote:    4e431fad67b..cc7848da115  JOSH_PUSH -> sync-from-ra
remote: REWRITE(1455ee0766bcb6075e6fdf797d24953f0af52b20 -> bf5c131477627b5e6273d5338d6b3ea137db4d41)
remote: 
remote: 
To http://localhost:42042/lnicola/rust.git:rev(f5a9250147f6569d8d89334dc9cca79c0322729f:prefix=src/tools/rust-analyzer):/src/tools/rust-analyzer.git
   16b22118c3..1455ee0766  HEAD -> sync-from-ra

Error: Josh created a non-roundtrip push! Do NOT merge this into rustc!

$ git rev-parse HEAD
1455ee0766bcb6075e6fdf797d24953f0af52b20
$ git rev-parse FETCH_HEAD
56021a0d6123ced47c4db622f5b7f56a5f9ae97f
$ git log
commit 1455ee0766bcb6075e6fdf797d24953f0af52b20 (HEAD -> josh)
Merge: a2dfd9785f 16b22118c3
Author: Laurențiu Nicola
Date:   Sun Apr 7 19:23:56 2024 +0300

    Merge from downstream

commit a2dfd9785f167413355f98cc06e7cc7cb6b6a94b
Author: Laurențiu Nicola
Date:   Sun Apr 7 19:23:54 2024 +0300

    Preparing for merge from downstream

commit 56021a0d6123ced47c4db622f5b7f56a5f9ae97f (mine/josh)
Author: Laurențiu Nicola
Date:   Sun Apr 7 11:41:39 2024 +0300

    Use josh for subtree syncs

So it's exactly the two pull commits that are missing. (This time I tried a pull, then push on the same branch). Yet here they are: https://github.com/lnicola/rust/commits/sync-from-ra/.

Copy link
Member

@RalfJung RalfJung Apr 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very strange. I don't know what to take from your git rev-parse, what do those show?

You marked the PR as "ready for review" -- did you make more progress here?
I can take a look eventually but probably not this week.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very strange. I don't know what to take from your git rev-parse, what do those show?

HEAD is the "Merge from downstream" commit, FETCH_HEAD is the josh change. Or maybe I misunderstood the question?

You marked the PR as "ready for review" -- did you make more progress here?

No, but I think it's a good start, and the FETCH_HEAD thing could conceivably be explained by my disk space problems. And it will be a while before I can try it again (since I have to reinstall and all that).

bail!("Josh created a non-roundtrip push! Do NOT merge this into rustc!");
}
println!("Confirmed that the push round-trips back to rust-analyzer properly. Please create a rustc PR:");
// https://github.com/github-linguist/linguist/compare/master...octocat:linguist:master
let fork_path = rust_fork.replace('/', ":");
println!(
" https://github.com/rust-lang/rust/compare/{fork_path}:{branch}?quick_pull=1&title=Subtree+update+of+rust-analyzer&body=r?+@ghost"
);

drop(josh);
Ok(())
}
}

/// Used for rustc syncs.
const JOSH_FILTER: &str =
":rev(55d9a533b309119c8acd13061581b43ae8840823:prefix=src/tools/rust-analyzer):/src/tools/rust-analyzer";
const JOSH_PORT: &str = "42042";

fn start_josh() -> anyhow::Result<impl Drop> {
// Determine cache directory.
let local_dir = {
let user_dirs = ProjectDirs::from("org", "rust-lang", "rust-analyzer-josh").unwrap();
user_dirs.cache_dir().to_owned()
};

// Start josh, silencing its output.
let mut cmd = Command::new("josh-proxy");
cmd.arg("--local").arg(local_dir);
cmd.arg("--remote").arg("https://github.com");
cmd.arg("--port").arg(JOSH_PORT);
cmd.arg("--no-background");
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
let josh = cmd.spawn().context("failed to start josh-proxy, make sure it is installed")?;
// Give it some time so hopefully the port is open. (100ms was not enough.)
thread::sleep(Duration::from_millis(200));

Ok(JodChild(josh))
}