Skip to content
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ If you need to specify a more complex Josh `filter`, use `filter` field in the c

The `init` command will also create an empty `rust-version` file (if it doesn't already exist) that stores the last upstream `rustc` SHA that was synced in the subtree.

The [`josh-sync.example.toml`](josh-sync.example.toml) file contains all the things that can be configured.

## Performing pull

A pull operation fetches changes to the subtree subdirectory that were performed in `rust-lang/rust` and merges them into the subtree repository. After performing a pull, a pull request is sent against the *subtree repository*. We *pull from rustc*.
Expand All @@ -25,15 +27,20 @@ A pull operation fetches changes to the subtree subdirectory that were performed
2) Create a new branch that will be used for the subtree PR, e.g. `pull`
3) Run `rustc-josh-sync pull`
4) Send a PR to the subtree repository
- Note that `rustc-josh-sync` can do this for you if you have the [gh](https://cli.github.com/) CLI tool installed.

- Note that `rustc-josh-sync` can do this for you if you have the [gh](https://cli.github.com/) CLI tool installed.

You can also configure a set of postprocessing operations to be performed after a successful pull using the `post-pull` configuration.

## Performing push

A push operation takes changes performed in the subtree repository and merges them into the subtree subdirectory of the `rust-lang/rust` repository. After performing a push, a push request is sent against the *rustc repository*. We *push to rustc*.

1) Checkout the latest default branch of the subtree
2) Run `rustc-josh-sync push <branch> <your-github-username>`
- The branch with the push contents will be created in `https://github.com/<your-github-username>/rust` fork, in the `<branch>` branch.

- The branch with the push contents will be created in `https://github.com/<your-github-username>/rust` fork, in the `<branch>` branch.

3) Send a PR to [rust-lang/rust]

## Automating pulls on CI
Expand Down
7 changes: 7 additions & 0 deletions josh-sync.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ path = "library/stdarch"
# Optionally, you can specify the complete Josh filter for complex cases
# Note that this option is mutually exclusive with `path`
#filter = ...

# Optionally, you can specify a set of commands executed after a successful pull.
# If the executed command changes the local git state (performs some modifications to files that
# were already tracked), then a new commit with the given message will be created.
#[[post-pull]]
#cmd = ["cargo", "fmt"]
#commit-message = "reformat"
1 change: 1 addition & 0 deletions src/bin/rustc_josh_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ fn main() -> anyhow::Result<()> {
repo: "<repository-name>".to_string(),
path: Some("<relative-subtree-path>".to_string()),
filter: None,
post_pull: vec![],
};
config
.write(Path::new(DEFAULT_CONFIG_PATH))
Expand Down
19 changes: 19 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Context;
use std::path::Path;

#[derive(serde::Serialize, serde::Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct JoshConfig {
#[serde(default = "default_org")]
pub org: String,
Expand All @@ -12,6 +13,24 @@ pub struct JoshConfig {
/// Optional filter specification for Josh.
/// It cannot be used together with `path`.
pub filter: Option<String>,
/// Operation(s) that should be performed after a pull.
/// Can be used to post-process the state of the repository after a pull happens.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub post_pull: Vec<PostPullOperation>,
}

/// Execute an operation after a pull, and if something changes in the local git state,
/// perform a commit.
#[derive(serde::Serialize, serde::Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PostPullOperation {
/// Execute a command with these arguments
/// At least one argument has to be present.
/// You can run e.g. bash if you want to do more complicated stuff.
pub cmd: Vec<String>,
/// If the git state has changed after `cmd`, add all changes to the index (`git add -u`)
/// and create a commit with the following commit message.
pub commit_message: String,
}

impl JoshConfig {
Expand Down
45 changes: 35 additions & 10 deletions src/sync.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::SyncContext;
use crate::config::PostPullOperation;
use crate::josh::JoshProxy;
use crate::utils::{ensure_clean_git_state, prompt};
use crate::utils::{get_current_head_sha, run_command_at};
Expand Down Expand Up @@ -65,7 +66,7 @@ impl GitSync {
.to_owned()
};

ensure_clean_git_state(self.verbose);
ensure_clean_git_state(self.verbose)?;

// Make sure josh is running.
let josh = self
Expand Down Expand Up @@ -220,17 +221,21 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue
// But it can be more tricky - we can have only empty merge/rollup merge commits from
// rustc, so a merge was created, but the in-tree diff can still be empty.
// In that case we also bail.
// `git diff --exit-code` "succeeds" if the diff is empty.
if run_command(
&["git", "diff", "--exit-code", &sha_pre_merge],
self.verbose,
)
.is_ok()
{
if self.has_empty_diff(&sha_pre_merge) {
eprintln!("Only empty changes were pulled. Rolling back the preparation commit.");
return Err(RustcPullError::NothingToPull);
}

println!("Pull finished! Current HEAD is {current_sha}");

if !self.context.config.post_pull.is_empty() {
println!("Running post-pull operation(s)");

for op in &self.context.config.post_pull {
self.run_post_pull_op(&op)?;
}
}

git_reset.disarm();

// Check that the number of roots did not change.
Expand All @@ -241,14 +246,13 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue
.into());
}

println!("Pull finished! Current HEAD is {current_sha}");
Ok(PullResult {
merge_commit_message: merge_message,
})
}

pub fn rustc_push(&self, username: &str, branch: &str) -> anyhow::Result<()> {
ensure_clean_git_state(self.verbose);
ensure_clean_git_state(self.verbose)?;

let base_upstream_sha = self.context.last_upstream_sha.clone().unwrap_or_default();

Expand Down Expand Up @@ -341,6 +345,27 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue

Ok(())
}

fn has_empty_diff(&self, baseline_sha: &str) -> bool {
// `git diff --exit-code` "succeeds" if the diff is empty.
run_command(&["git", "diff", "--exit-code", baseline_sha], self.verbose).is_ok()
}

fn run_post_pull_op(&self, op: &PostPullOperation) -> anyhow::Result<()> {
let head = get_current_head_sha(self.verbose)?;
run_command(op.cmd.iter().map(|s| s.as_str()).collect::<Vec<_>>(), true)?;
if !self.has_empty_diff(&head) {
println!(
"`{}` changed something, committing with message `{}`",
op.cmd.join(" "),
op.commit_message
);
run_command(["git", "add", "-u"], self.verbose)?;
run_command(["git", "commit", "-m", &op.commit_message], self.verbose)?;
}

Ok(())
}
}

/// Find a rustc repo we can do our push preparation in.
Expand Down
8 changes: 6 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ fn run_command_inner<'a, Args: AsRef<[&'a str]>>(
}

/// Fail if there are files that need to be checked in.
pub fn ensure_clean_git_state(verbose: bool) {
pub fn ensure_clean_git_state(verbose: bool) -> anyhow::Result<()> {
let read = run_command(
["git", "status", "--untracked-files=no", "--porcelain"],
verbose,
)
.expect("cannot figure out if git state is clean");
assert!(read.is_empty(), "working directory must be clean");
if !read.is_empty() {
Err(anyhow::anyhow!("working directory must be clean"))
} else {
Ok(())
}
}

pub fn get_current_head_sha(verbose: bool) -> anyhow::Result<String> {
Expand Down
Loading