123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- //! A scheduled job to post a PR to update the documentation on rust-lang/rust.
- // 暂时允许unreachable code, 因为我们目前不需要这个任务,只是为了方便注释它。
- #![allow(unreachable_code)]
- use crate::github::{self, GitTreeEntry, GithubClient, Issue, Repository};
- use crate::jobs::Job;
- use anyhow::Context;
- use anyhow::Result;
- use async_trait::async_trait;
- use std::fmt::Write;
- /// This is the repository where the commits will be created.
- const WORK_REPO: &str = "rustbot/rust";
- /// This is the repository where the PR will be created.
- const DEST_REPO: &str = "rust-lang/rust";
- /// This is the branch in `WORK_REPO` to create the commits.
- const BRANCH_NAME: &str = "docs-update";
- const SUBMODULES: &[&str] = &[
- "src/doc/book",
- "src/doc/edition-guide",
- "src/doc/embedded-book",
- "src/doc/nomicon",
- "src/doc/reference",
- "src/doc/rust-by-example",
- "src/doc/rustc-dev-guide",
- ];
- const TITLE: &str = "Update books";
- /// 这个任务用于更新文档仓库的submodule,并在文档仓库创建PR
- pub struct DocsUpdateJob;
- #[async_trait]
- impl Job for DocsUpdateJob {
- fn name(&self) -> &'static str {
- "docs_update"
- }
- async fn run(
- &self,
- _ctx: &super::Context,
- _metadata: &serde_json::Value,
- ) -> anyhow::Result<()> {
- // 我们目前暂时不需要这个,因此直接返回Ok
- return Ok(());
- // Only run every other week. Doing it every week can be a bit noisy, and
- // (rarely) a PR can take longer than a week to merge (like if there are
- // CI issues). `Schedule` does not allow expressing this, so check it
- // manually.
- //
- // This is set to run the first week after a release, and the week just
- // before a release. That allows getting the latest changes in the next
- // release, accounting for possibly taking a few days for the PR to land.
- let today = chrono::Utc::today().naive_utc();
- let base = chrono::naive::NaiveDate::from_ymd(2015, 12, 10);
- let duration = today.signed_duration_since(base);
- let weeks = duration.num_weeks();
- if weeks % 2 != 0 {
- tracing::trace!("skipping job, this is an odd week");
- return Ok(());
- }
- tracing::trace!("starting docs-update");
- docs_update()
- .await
- .context("failed to process docs update")?;
- Ok(())
- }
- }
- pub async fn docs_update() -> Result<Option<Issue>> {
- let gh = GithubClient::new_from_env();
- let dest_repo = gh.repository(DEST_REPO).await?;
- let work_repo = gh.repository(WORK_REPO).await?;
- let updates = get_submodule_updates(&gh, &dest_repo).await?;
- if updates.is_empty() {
- tracing::trace!("no updates this week?");
- return Ok(None);
- }
- create_commit(&gh, &dest_repo, &work_repo, &updates).await?;
- Ok(Some(create_pr(&gh, &dest_repo, &updates).await?))
- }
- struct Update {
- path: String,
- new_hash: String,
- pr_body: String,
- }
- async fn get_submodule_updates(
- gh: &GithubClient,
- repo: &github::Repository,
- ) -> Result<Vec<Update>> {
- let mut updates = Vec::new();
- for submodule_path in SUBMODULES {
- tracing::trace!("checking submodule {submodule_path}");
- let submodule = repo.submodule(gh, submodule_path, None).await?;
- let submodule_repo = submodule.repository(gh).await?;
- let latest_commit = submodule_repo
- .get_reference(gh, &format!("heads/{}", submodule_repo.default_branch))
- .await?;
- if submodule.sha == latest_commit.object.sha {
- tracing::trace!(
- "skipping submodule {submodule_path}, no changes sha={}",
- submodule.sha
- );
- continue;
- }
- let current_hash = submodule.sha;
- let new_hash = latest_commit.object.sha;
- let pr_body = generate_pr_body(gh, &submodule_repo, ¤t_hash, &new_hash).await?;
- let update = Update {
- path: submodule.path,
- new_hash,
- pr_body,
- };
- updates.push(update);
- }
- Ok(updates)
- }
- async fn generate_pr_body(
- gh: &GithubClient,
- repo: &github::Repository,
- oldest: &str,
- newest: &str,
- ) -> Result<String> {
- let recent_commits: Vec<_> = repo
- .recent_commits(gh, &repo.default_branch, oldest, newest)
- .await?;
- if recent_commits.is_empty() {
- anyhow::bail!(
- "unexpected empty set of commits for {} oldest={oldest} newest={newest}",
- repo.full_name
- );
- }
- let mut body = format!(
- "## {}\n\
- \n\
- {} commits in {}..{}\n\
- {} to {}\n\
- \n",
- repo.full_name,
- recent_commits.len(),
- oldest,
- newest,
- recent_commits.first().unwrap().committed_date,
- recent_commits.last().unwrap().committed_date,
- );
- for commit in recent_commits {
- write!(body, "- {}", commit.title).unwrap();
- if let Some(num) = commit.pr_num {
- write!(body, " ({}#{})", repo.full_name, num).unwrap();
- }
- body.push('\n');
- }
- Ok(body)
- }
- async fn create_commit(
- gh: &GithubClient,
- dest_repo: &Repository,
- rust_repo: &Repository,
- updates: &[Update],
- ) -> Result<()> {
- let master_ref = dest_repo
- .get_reference(gh, &format!("heads/{}", dest_repo.default_branch))
- .await?;
- let master_commit = rust_repo.git_commit(gh, &master_ref.object.sha).await?;
- let tree_entries: Vec<_> = updates
- .iter()
- .map(|update| GitTreeEntry {
- path: update.path.clone(),
- mode: "160000".to_string(),
- object_type: "commit".to_string(),
- sha: update.new_hash.clone(),
- })
- .collect();
- let new_tree = rust_repo
- .update_tree(gh, &master_commit.tree.sha, &tree_entries)
- .await?;
- let commit = rust_repo
- .create_commit(gh, TITLE, &[&master_ref.object.sha], &new_tree.sha)
- .await?;
- rust_repo
- .update_reference(gh, &format!("heads/{BRANCH_NAME}"), &commit.sha)
- .await?;
- Ok(())
- }
- async fn create_pr(gh: &GithubClient, dest_repo: &Repository, updates: &[Update]) -> Result<Issue> {
- let mut body = String::new();
- for update in updates {
- write!(body, "{}\n", update.pr_body).unwrap();
- }
- let username = WORK_REPO.split('/').next().unwrap();
- let head = format!("{username}:{BRANCH_NAME}");
- let pr = dest_repo
- .new_pr(gh, TITLE, &head, &dest_repo.default_branch, &body)
- .await?;
- tracing::debug!("created PR {}", pr.html_url);
- Ok(pr)
- }
|