1use std::collections::{HashMap, HashSet};
4use std::fmt::{self, Debug, Display, Write};
5use std::io;
6use std::path::PathBuf;
7use std::process::{Command, Stdio};
8use std::str::FromStr;
9use std::time::SystemTime;
10
11use cursive_core::theme::Effect;
12use cursive_core::utils::markup::StyledString;
13use git_branchless_opts::Revset;
14use git_branchless_test::{
15 run_tests, FixInfo, ResolvedTestOptions, TestOutput, TestResults, TestStatus,
16 TestingAbortedError, Verbosity,
17};
18use itertools::Itertools;
19use lazy_static::lazy_static;
20use lib::core::check_out::CheckOutCommitOptions;
21use lib::core::dag::{CommitSet, Dag};
22use lib::core::effects::{Effects, OperationType, WithProgress};
23use lib::core::eventlog::EventLogDb;
24use lib::core::formatting::StyledStringBuilder;
25use lib::core::rewrite::{
26 execute_rebase_plan, BuildRebasePlanError, BuildRebasePlanOptions, ExecuteRebasePlanOptions,
27 ExecuteRebasePlanResult, RebasePlanBuilder, RebasePlanPermissions, RepoResource,
28};
29use lib::git::{Commit, GitRunInfo, MaybeZeroOid, NonZeroOid, Repo, RepoError, TestCommand};
30use lib::try_exit_code;
31use lib::util::{ExitCode, EyreExitOr};
32use rayon::ThreadPoolBuilder;
33use regex::bytes::Regex;
34use serde::{Deserialize, Serialize};
35use thiserror::Error;
36use tracing::{instrument, warn};
37
38use crate::{CommitStatus, CreateStatus, Forge, SubmitOptions, SubmitStatus, STYLE_PUSHED};
39
40#[derive(Clone, Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
43#[serde(transparent)]
44pub struct Id(pub String);
45
46impl Display for Id {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 let Self(id) = self;
49 write!(f, "D{id}")
50 }
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
54#[serde(transparent)]
55struct Phid(pub String);
56
57#[derive(Clone, Debug, Default, Serialize, Eq, PartialEq)]
58struct DifferentialQueryRequest {
59 ids: Vec<Id>,
60 phids: Vec<Phid>,
61}
62
63#[derive(Debug, Serialize, Eq, PartialEq)]
64struct DifferentialEditRequest {
65 #[serde(rename = "objectIdentifier")]
66 id: Id, transactions: Vec<DifferentialEditTransaction>,
68}
69
70#[derive(Debug, Default, Serialize, Eq, PartialEq)]
71struct DifferentialEditTransaction {
72 r#type: String,
73 value: Vec<Phid>,
74}
75
76#[derive(Debug, Deserialize)]
77struct ConduitResponse<T> {
78 #[serde(rename = "errorMessage")]
79 error_message: Option<String>,
80 response: Option<T>,
81}
82
83impl<T> ConduitResponse<T> {
84 fn check_err(self) -> std::result::Result<T, String> {
85 let Self {
86 error_message,
87 response,
88 } = self;
89 match error_message {
90 Some(error_message) => Err(error_message),
91 None => match response {
92 None => Err("(no error message)".to_string()),
93 Some(response) => Ok(response),
94 },
95 }
96 }
97}
98
99impl<T: Default> Default for ConduitResponse<T> {
100 fn default() -> Self {
101 Self {
102 error_message: Default::default(),
103 response: Default::default(),
104 }
105 }
106}
107
108#[derive(Debug, Deserialize)]
109struct DifferentialQueryRevisionResponse {
110 id: Id,
111 phid: Phid,
112
113 #[serde(default)]
114 hashes: Vec<(String, String)>,
115
116 #[serde(default)]
117 auxiliary: DifferentialQueryAuxiliaryResponse,
118}
119
120#[derive(Debug, Default, Deserialize)]
121struct DifferentialQueryAuxiliaryResponse {
122 #[serde(rename = "phabricator:depends-on")]
124 phabricator_depends_on: Vec<Phid>,
125}
126
127#[allow(missing_docs)]
129#[derive(Debug, Error)]
130pub enum Error {
131 #[error("no working copy for repository at path: {}", .repo_path.display())]
132 NoWorkingCopy { repo_path: PathBuf },
133
134 #[error("could not iterate commits: {0}")]
135 IterCommits(#[source] eyre::Error),
136
137 #[error("could not look up commits: {0}")]
138 LookUpCommits(#[source] RepoError),
139
140 #[error("no commit with hash {commit_oid:?}: {source}")]
141 NoSuchCommit {
142 source: RepoError,
143 commit_oid: NonZeroOid,
144 },
145
146 #[error("invocation to `arc {args}` failed: {source}", args = args.join(" "))]
147 InvokeArc {
148 source: io::Error,
149 args: Vec<String>,
150 },
151
152 #[error("communication with `arc {args}` failed: {source}", args = args.join(" "))]
153 CommunicateWithArc {
154 source: serde_json::Error,
155 args: Vec<String>,
156 },
157
158 #[error("could not create phab for {commit_oid} when running `arc {args}` (exit code {exit_code}): {message}", args = args.join(" "))]
159 CreatePhab {
160 exit_code: i32,
161 message: String,
162 commit_oid: NonZeroOid,
163 args: Vec<String>,
164 },
165
166 #[error("could not query dependencies when running `arc {args}` (exit code {exit_code}): {message}", args = args.join(" "))]
167 QueryDependencies {
168 exit_code: i32,
169 message: String,
170 args: Vec<String>,
171 },
172
173 #[error("could not update dependencies when running `arc {args}` (exit code {exit_code}): {message}", args = args.join(" "))]
174 UpdateDependencies {
175 exit_code: i32,
176 message: String,
177 args: Vec<String>,
178 },
179
180 #[error("could not parse response when running `arc {args}`: {source}; with output: {output}", args = args.join(" "))]
181 ParseResponse {
182 source: serde_json::Error,
183 output: String,
184 args: Vec<String>,
185 },
186
187 #[error("error when calling Conduit API with request {request:?}: {message}")]
188 Conduit {
189 request: Box<dyn Debug + Send + Sync>,
190 message: String,
191 },
192
193 #[error("could not make transaction ID: {source}")]
194 MakeTransactionId { source: eyre::Error },
195
196 #[error("could not execute `arc diff` on commits: {source}")]
197 ExecuteArcDiff { source: eyre::Error },
198
199 #[error("could not verify permissions to rewrite commits: {source}")]
200 VerifyPermissions { source: eyre::Error },
201
202 #[error("could not build rebase plan")]
203 BuildRebasePlan(BuildRebasePlanError),
204
205 #[error("failed to rewrite commits with exit code {}", exit_code.0)]
206 RewriteCommits { exit_code: ExitCode },
207
208 #[error(transparent)]
209 Fmt(#[from] fmt::Error),
210
211 #[error(transparent)]
212 DagError(#[from] eden_dag::Error),
213}
214
215pub type Result<T> = std::result::Result<T, Error>;
217
218pub const SHOULD_MOCK_ENV_KEY: &str = "BRANCHLESS_SUBMIT_PHABRICATOR_MOCK";
221
222fn should_mock() -> bool {
223 std::env::var_os(SHOULD_MOCK_ENV_KEY).is_some()
224}
225
226#[allow(missing_docs)]
231#[derive(Debug)]
232pub struct PhabricatorForge<'a> {
233 pub effects: &'a Effects,
234 pub git_run_info: &'a GitRunInfo,
235 pub repo: &'a Repo,
236 pub dag: &'a mut Dag,
237 pub event_log_db: &'a EventLogDb<'a>,
238 pub revset: &'a Revset,
239}
240
241impl Forge for PhabricatorForge<'_> {
242 #[instrument]
243 fn query_status(
244 &mut self,
245 commit_set: CommitSet,
246 ) -> eyre::Result<std::result::Result<HashMap<NonZeroOid, CommitStatus>, ExitCode>> {
247 let commit_oids = self.dag.commit_set_to_vec(&commit_set)?;
248 let commit_oid_to_revision: HashMap<NonZeroOid, Option<Id>> = commit_oids
249 .into_iter()
250 .map(|commit_oid| -> eyre::Result<_> {
251 let revision_id = self.get_revision_id(commit_oid)?;
252 Ok((commit_oid, revision_id))
253 })
254 .try_collect()?;
255
256 let revisions = if should_mock() {
257 Default::default()
258 } else {
259 self.query_revisions(&DifferentialQueryRequest {
260 ids: commit_oid_to_revision.values().flatten().cloned().collect(),
261 phids: Default::default(),
262 })?
263 };
264 let commit_hashes: HashMap<Id, NonZeroOid> = revisions
265 .into_iter()
266 .filter_map(|item| {
267 let hashes: HashMap<String, String> = item.hashes.iter().cloned().collect();
268 if hashes.is_empty() {
269 None
270 } else {
271 match hashes.get("gtcm") {
273 None => {
274 warn!(?item, "No Git commit hash in item");
275 None
276 }
277 Some(commit_oid) => match NonZeroOid::from_str(commit_oid.as_str()) {
278 Ok(commit_oid) => Some((item.id, commit_oid)),
279 Err(err) => {
280 warn!(?err, "Couldn't parse Git commit OID");
281 None
282 }
283 },
284 }
285 }
286 })
287 .collect();
288
289 let statuses = commit_oid_to_revision
290 .into_iter()
291 .map(|(commit_oid, id)| {
292 let status = CommitStatus {
293 submit_status: match id {
294 Some(id) => match commit_hashes.get(&id) {
295 Some(remote_commit_oid) => {
296 if remote_commit_oid == &commit_oid {
297 SubmitStatus::UpToDate
298 } else {
299 SubmitStatus::NeedsUpdate
300 }
301 }
302 None => {
303 warn!(?commit_oid, ?id, "No remote commit hash found for commit");
304 SubmitStatus::NeedsUpdate
305 }
306 },
307 None => SubmitStatus::Unsubmitted,
308 },
309 remote_name: None,
310 local_commit_name: None,
311 remote_commit_name: None,
312 };
313 (commit_oid, status)
314 })
315 .collect();
316 Ok(Ok(statuses))
317 }
318
319 #[instrument]
320 fn create(
321 &mut self,
322 commits: HashMap<NonZeroOid, CommitStatus>,
323 options: &SubmitOptions,
324 ) -> eyre::Result<std::result::Result<HashMap<NonZeroOid, CreateStatus>, ExitCode>> {
325 let SubmitOptions {
326 create: _,
327 draft,
328 execution_strategy,
329 num_jobs,
330 message: _,
331 } = options;
332
333 let commit_set = commits.keys().copied().collect();
334 let commit_oids = self.dag.sort(&commit_set).map_err(Error::IterCommits)?;
335 let commits: Vec<Commit> = commit_oids
336 .iter()
337 .map(|commit_oid| self.repo.find_commit_or_fail(*commit_oid))
338 .collect::<std::result::Result<_, _>>()
339 .map_err(Error::LookUpCommits)?;
340 let now = SystemTime::now();
341 let event_tx_id = self
342 .event_log_db
343 .make_transaction_id(now, "phabricator create")
344 .map_err(|err| Error::MakeTransactionId { source: err })?;
345 let build_options = BuildRebasePlanOptions {
346 force_rewrite_public_commits: false,
347 dump_rebase_constraints: false,
348 dump_rebase_plan: false,
349 detect_duplicate_commits_via_patch_id: false,
350 };
351 let execute_options = ExecuteRebasePlanOptions {
352 now,
353 event_tx_id,
354 preserve_timestamps: true,
355 force_in_memory: true,
356 force_on_disk: false,
357 resolve_merge_conflicts: false,
358 check_out_commit_options: CheckOutCommitOptions {
359 render_smartlog: false,
360 ..Default::default()
361 },
362 };
363 let permissions =
364 RebasePlanPermissions::verify_rewrite_set(self.dag, build_options, &commit_set)
365 .map_err(|err| Error::VerifyPermissions { source: err })?
366 .map_err(Error::BuildRebasePlan)?;
367 let command = if !should_mock() {
368 let mut args = vec!["arc", "diff", "--create", "--verbatim", "--allow-untracked"];
369 if *draft {
370 args.push("--draft");
371 }
372 args.extend(["--", "HEAD^"]);
373 TestCommand::Args(args.into_iter().map(ToString::to_string).collect())
374 } else {
375 TestCommand::String(
376 r#"git commit --amend --message "$(git show --no-patch --format=%B HEAD)
377
378Differential Revision: https://phabricator.example.com/D000$(git rev-list --count HEAD)
379 "
380 "#
381 .to_string(),
382 )
383 };
384
385 let test_results = match run_tests(
386 now,
387 self.effects,
388 self.git_run_info,
389 self.dag,
390 self.repo,
391 self.event_log_db,
392 self.revset,
393 &commits,
394 &ResolvedTestOptions {
395 command,
396 execution_strategy: *execution_strategy,
397 search_strategy: None,
398 is_dry_run: false,
399 use_cache: false,
400 is_interactive: false,
401 num_jobs: *num_jobs,
402 verbosity: Verbosity::None,
403 fix_options: Some((execute_options.clone(), permissions.clone())),
404 },
405 ) {
406 Ok(Ok(test_results)) => test_results,
407 Ok(Err(exit_code)) => return Ok(Err(exit_code)),
408 Err(err) => return Err(Error::ExecuteArcDiff { source: err }.into()),
409 };
410
411 let TestResults {
412 search_bounds: _,
413 test_outputs,
414 testing_aborted_error,
415 } = test_results;
416 if let Some(testing_aborted_error) = testing_aborted_error {
417 let TestingAbortedError {
418 commit_oid,
419 exit_code,
420 } = testing_aborted_error;
421 writeln!(
422 self.effects.get_output_stream(),
423 "Uploading was aborted with exit code {exit_code} due to commit {}",
424 self.effects.get_glyphs().render(
425 self.repo
426 .friendly_describe_commit_from_oid(self.effects.get_glyphs(), commit_oid)?
427 )?,
428 )?;
429 return Ok(Err(ExitCode(1)));
430 }
431
432 let rebase_plan = {
433 let mut builder = RebasePlanBuilder::new(self.dag, permissions);
434 for (commit_oid, test_output) in test_outputs {
435 let head_commit_oid = match test_output.test_status {
436 TestStatus::CheckoutFailed
437 | TestStatus::SpawnTestFailed(_)
438 | TestStatus::TerminatedBySignal
439 | TestStatus::AlreadyInProgress
440 | TestStatus::ReadCacheFailed(_)
441 | TestStatus::Indeterminate { .. }
442 | TestStatus::Abort { .. }
443 | TestStatus::Failed { .. } => {
444 self.render_failed_test(commit_oid, &test_output)?;
445 return Ok(Err(ExitCode(1)));
446 }
447 TestStatus::Passed {
448 cached: _,
449 fix_info:
450 FixInfo {
451 head_commit_oid,
452 snapshot_tree_oid: _,
453 },
454 interactive: _,
455 } => head_commit_oid,
456 };
457
458 let commit = self.repo.find_commit_or_fail(commit_oid)?;
459 builder.move_subtree(commit.get_oid(), commit.get_parent_oids())?;
460 builder.replace_commit(commit.get_oid(), head_commit_oid.unwrap_or(commit_oid))?;
461 }
462
463 let pool = ThreadPoolBuilder::new().build()?;
464 let repo_pool = RepoResource::new_pool(self.repo)?;
465 match builder.build(self.effects, &pool, &repo_pool)? {
466 Ok(Some(rebase_plan)) => rebase_plan,
467 Ok(None) => return Ok(Ok(Default::default())),
468 Err(err) => {
469 err.describe(self.effects, self.repo, self.dag)?;
470 return Ok(Err(ExitCode(1)));
471 }
472 }
473 };
474
475 let rewritten_oids = match execute_rebase_plan(
476 self.effects,
477 self.git_run_info,
478 self.repo,
479 self.event_log_db,
480 &rebase_plan,
481 &execute_options,
482 )? {
483 ExecuteRebasePlanResult::Succeeded {
484 rewritten_oids: Some(rewritten_oids),
485 } => rewritten_oids,
486 ExecuteRebasePlanResult::Succeeded {
487 rewritten_oids: None,
488 } => {
489 warn!("No rewritten commit OIDs were produced by rebase plan execution");
490 Default::default()
491 }
492 ExecuteRebasePlanResult::DeclinedToMerge {
493 failed_merge_info: _,
494 } => {
495 writeln!(
496 self.effects.get_error_stream(),
497 "BUG: Merge failed, but rewording shouldn't cause any merge failures."
498 )?;
499 return Ok(Err(ExitCode(1)));
500 }
501 ExecuteRebasePlanResult::Failed { exit_code } => {
502 return Ok(Err(exit_code));
503 }
504 };
505
506 let mut create_statuses = HashMap::new();
507 for commit_oid in commit_oids {
508 let final_commit_oid = match rewritten_oids.get(&commit_oid) {
509 Some(MaybeZeroOid::NonZero(commit_oid)) => *commit_oid,
510 Some(MaybeZeroOid::Zero) => {
511 warn!(?commit_oid, "Commit was rewritten to the zero OID",);
512 commit_oid
513 }
514 None => commit_oid,
515 };
516 let local_branch_name = {
517 match self.get_revision_id(final_commit_oid)? {
518 Some(Id(id)) => format!("D{id}"),
519 None => {
520 writeln!(
521 self.effects.get_output_stream(),
522 "Failed to upload (link to newly-created revision not found in commit message): {}",
523 self.effects.get_glyphs().render(
524 self.repo.friendly_describe_commit_from_oid(
525 self.effects.get_glyphs(),
526 final_commit_oid
527 )?
528 )?,
529 )?;
530 return Ok(Err(ExitCode(1)));
531 }
532 }
533 };
534 create_statuses.insert(
535 commit_oid,
536 CreateStatus {
537 final_commit_oid,
538 local_commit_name: local_branch_name,
539 },
540 );
541 }
542
543 let final_commit_oids: CommitSet = create_statuses
544 .values()
545 .map(|create_status| {
546 let CreateStatus {
547 final_commit_oid,
548 local_commit_name: _,
549 } = create_status;
550 *final_commit_oid
551 })
552 .collect();
553 self.dag.sync_from_oids(
554 self.effects,
555 self.repo,
556 CommitSet::empty(),
557 final_commit_oids.clone(),
558 )?;
559 match self.update_dependencies(&final_commit_oids, &final_commit_oids)? {
560 Ok(()) => {}
561 Err(exit_code) => return Ok(Err(exit_code)),
562 }
563
564 Ok(Ok(create_statuses))
565 }
566
567 #[instrument]
568 fn update(
569 &mut self,
570 commits: HashMap<NonZeroOid, crate::CommitStatus>,
571 options: &SubmitOptions,
572 ) -> EyreExitOr<()> {
573 let SubmitOptions {
574 create: _,
575 draft: _,
576 execution_strategy,
577 num_jobs,
578 message,
579 } = options;
580
581 let commit_set = commits.keys().copied().collect();
582 let commit_oids = self.dag.sort(&commit_set)?;
584 let commits: Vec<_> = commit_oids
585 .into_iter()
586 .map(|commit_oid| self.repo.find_commit_or_fail(commit_oid))
587 .try_collect()?;
588
589 let now = SystemTime::now();
590 let event_tx_id = self
591 .event_log_db
592 .make_transaction_id(now, "phabricator update")?;
593 let build_options = BuildRebasePlanOptions {
594 force_rewrite_public_commits: false,
595 dump_rebase_constraints: false,
596 dump_rebase_plan: false,
597 detect_duplicate_commits_via_patch_id: false,
598 };
599 let execute_options = ExecuteRebasePlanOptions {
600 now,
601 event_tx_id,
602 preserve_timestamps: true,
603 force_in_memory: true,
604 force_on_disk: false,
605 resolve_merge_conflicts: false,
606 check_out_commit_options: CheckOutCommitOptions {
607 render_smartlog: false,
608 ..Default::default()
609 },
610 };
611 let permissions =
612 RebasePlanPermissions::verify_rewrite_set(self.dag, build_options, &commit_set)
613 .map_err(|err| Error::VerifyPermissions { source: err })?
614 .map_err(Error::BuildRebasePlan)?;
615 let test_options = ResolvedTestOptions {
616 command: if !should_mock() {
617 let mut args = vec![
618 "arc",
619 "diff",
620 "--head",
621 "HEAD",
622 "HEAD^",
623 "--allow-untracked",
624 ];
625 args.extend(match message {
626 Some(message) => ["-m", message.as_ref()],
627 None => ["-m", "update"],
628 });
629 TestCommand::Args(args.into_iter().map(ToString::to_string).collect())
630 } else {
631 TestCommand::String("echo Submitting $(git rev-parse HEAD)".to_string())
632 },
633 execution_strategy: *execution_strategy,
634 search_strategy: None,
635 is_dry_run: false,
636 use_cache: false,
637 is_interactive: false,
638 num_jobs: *num_jobs,
639 verbosity: Verbosity::None,
640 fix_options: Some((execute_options, permissions)),
641 };
642 let TestResults {
643 search_bounds: _,
644 test_outputs,
645 testing_aborted_error,
646 } = try_exit_code!(run_tests(
647 now,
648 self.effects,
649 self.git_run_info,
650 self.dag,
651 self.repo,
652 self.event_log_db,
653 self.revset,
654 &commits,
655 &test_options,
656 )?);
657 if let Some(testing_aborted_error) = testing_aborted_error {
658 let TestingAbortedError {
659 commit_oid,
660 exit_code,
661 } = testing_aborted_error;
662 writeln!(
663 self.effects.get_output_stream(),
664 "Updating was aborted with exit code {exit_code} due to commit {}",
665 self.effects.get_glyphs().render(
666 self.repo
667 .friendly_describe_commit_from_oid(self.effects.get_glyphs(), commit_oid)?
668 )?,
669 )?;
670 return Ok(Err(ExitCode(1)));
671 }
672
673 let (success_commits, failure_commits): (Vec<_>, Vec<_>) = test_outputs
674 .into_iter()
675 .partition(|(_commit_oid, test_output)| match test_output.test_status {
676 TestStatus::Passed { .. } => true,
677 TestStatus::CheckoutFailed
678 | TestStatus::SpawnTestFailed(_)
679 | TestStatus::TerminatedBySignal
680 | TestStatus::AlreadyInProgress
681 | TestStatus::ReadCacheFailed(_)
682 | TestStatus::Indeterminate { .. }
683 | TestStatus::Abort { .. }
684 | TestStatus::Failed { .. } => false,
685 });
686 if !failure_commits.is_empty() {
687 let effects = self.effects;
688 writeln!(
689 effects.get_output_stream(),
690 "Failed when running command: {}",
691 effects.get_glyphs().render(
692 StyledStringBuilder::new()
693 .append_styled(test_options.command.to_string(), Effect::Bold)
694 .build()
695 )?
696 )?;
697 for (commit_oid, test_output) in failure_commits {
698 self.render_failed_test(commit_oid, &test_output)?;
699 }
700 return Ok(Err(ExitCode(1)));
701 }
702
703 try_exit_code!(self.update_dependencies(
704 &success_commits
705 .into_iter()
706 .map(|(commit_oid, _test_output)| commit_oid)
707 .collect(),
708 &CommitSet::empty()
709 )?);
710 Ok(Ok(()))
711 }
712}
713
714impl PhabricatorForge<'_> {
715 fn query_revisions(
716 &self,
717 request: &DifferentialQueryRequest,
718 ) -> Result<Vec<DifferentialQueryRevisionResponse>> {
719 if request == &DifferentialQueryRequest::default() {
722 return Ok(Default::default());
723 }
724
725 let args = vec![
726 "call-conduit".to_string(),
727 "--".to_string(),
728 "differential.query".to_string(),
729 ];
730 let mut child = Command::new("arc")
731 .args(&args)
732 .stdin(Stdio::piped())
733 .stdout(Stdio::piped())
734 .stderr(Stdio::inherit())
735 .spawn()
736 .map_err(|err| Error::InvokeArc {
737 source: err,
738 args: args.clone(),
739 })?;
740 serde_json::to_writer_pretty(child.stdin.take().unwrap(), request).map_err(|err| {
741 Error::CommunicateWithArc {
742 source: err,
743 args: args.clone(),
744 }
745 })?;
746 let result = child.wait_with_output().map_err(|err| Error::InvokeArc {
747 source: err,
748 args: args.clone(),
749 })?;
750 if !result.status.success() {
751 return Err(Error::QueryDependencies {
752 exit_code: result.status.code().unwrap_or(-1),
753 message: String::from_utf8_lossy(&result.stdout).into_owned(),
754 args,
755 });
756 }
757
758 let output: ConduitResponse<Vec<DifferentialQueryRevisionResponse>> =
759 serde_json::from_slice(&result.stdout).map_err(|err| Error::ParseResponse {
760 source: err,
761 output: String::from_utf8_lossy(&result.stdout).into_owned(),
762 args: args.clone(),
763 })?;
764 let response = output.check_err().map_err(|message| Error::Conduit {
765 request: Box::new(request.clone()),
766 message,
767 })?;
768 Ok(response)
769 }
770
771 pub fn query_remote_dependencies(
773 &self,
774 commit_oids: HashSet<NonZeroOid>,
775 ) -> Result<HashMap<NonZeroOid, HashSet<NonZeroOid>>> {
776 let commit_oid_to_id: HashMap<NonZeroOid, Option<Id>> = {
778 let mut result = HashMap::new();
779 for commit_oid in commit_oids.iter().copied() {
780 let revision_id = self.get_revision_id(commit_oid)?;
781 result.insert(commit_oid, revision_id);
782 }
783 result
784 };
785
786 let id_to_commit_oid: HashMap<Id, NonZeroOid> = commit_oid_to_id
789 .iter()
790 .filter_map(|(commit_oid, id)| id.as_ref().map(|v| (v.clone(), *commit_oid)))
791 .collect();
792
793 let query_ids: Vec<Id> = commit_oid_to_id
795 .values()
796 .filter_map(|id| id.as_ref().cloned())
797 .collect();
798
799 let revisions = self.query_revisions(&DifferentialQueryRequest {
800 ids: query_ids,
801 phids: Default::default(),
802 })?;
803
804 let dependency_phids: HashMap<Id, Vec<Phid>> = revisions
806 .into_iter()
807 .map(|revision| {
808 let DifferentialQueryRevisionResponse {
809 id,
810 phid: _,
811 hashes: _,
812 auxiliary:
813 DifferentialQueryAuxiliaryResponse {
814 phabricator_depends_on,
815 },
816 } = revision;
817 (id, phabricator_depends_on)
818 })
819 .collect();
820
821 let dependency_ids: HashMap<Id, Vec<Id>> = {
823 let all_phids: Vec<Phid> = dependency_phids.values().flatten().cloned().collect();
824 let revisions = self.query_revisions(&DifferentialQueryRequest {
825 ids: Default::default(),
826 phids: all_phids,
827 })?;
828 let phid_to_id: HashMap<Phid, Id> = revisions
829 .into_iter()
830 .map(|revision| {
831 let DifferentialQueryRevisionResponse {
832 id,
833 phid,
834 hashes: _,
835 auxiliary: _,
836 } = revision;
837 (phid, id)
838 })
839 .collect();
840 dependency_phids
841 .into_iter()
842 .map(|(id, dependency_phids)| {
843 (
844 id,
845 dependency_phids
846 .into_iter()
847 .filter_map(|dependency_phid| phid_to_id.get(&dependency_phid))
848 .cloned()
849 .collect(),
850 )
851 })
852 .collect()
853 };
854
855 let result: HashMap<NonZeroOid, HashSet<NonZeroOid>> = commit_oid_to_id
859 .into_iter()
860 .map(|(commit_oid, id)| {
861 let dependency_ids = match id {
862 None => Default::default(),
863 Some(id) => match dependency_ids.get(&id) {
864 None => Default::default(),
865 Some(dependency_ids) => dependency_ids
866 .iter()
867 .filter_map(|dependency_id| id_to_commit_oid.get(dependency_id))
868 .copied()
869 .collect(),
870 },
871 };
872 (commit_oid, dependency_ids)
873 })
874 .collect();
875 Ok(result)
876 }
877
878 fn update_dependencies(
879 &self,
880 commits: &CommitSet,
881 newly_created_commits: &CommitSet,
882 ) -> eyre::Result<std::result::Result<(), ExitCode>> {
883 let commit_oids = self.dag.sort(commits)?;
886
887 let (effects, progress) = self.effects.start_operation(OperationType::UpdateCommits);
888
889 let draft_commits = self.dag.query_draft_commits()?.union(newly_created_commits);
891
892 for commit_oid in commit_oids.into_iter().with_progress(progress) {
893 let id = match self.get_revision_id(commit_oid)? {
894 Some(id) => id,
895 None => {
896 warn!(?commit_oid, "No Phabricator commit ID for latest commit");
897 continue;
898 }
899 };
900 let commit = self.repo.find_commit_or_fail(commit_oid)?;
901 let parent_oids = commit.get_parent_oids();
902
903 let mut parent_revision_ids = Vec::new();
904 for parent_oid in parent_oids {
905 if !self.dag.set_contains(&draft_commits, parent_oid)? {
906 continue;
909 }
910 let parent_revision_id = match self.get_revision_id(parent_oid)? {
911 Some(id) => id,
912 None => continue,
913 };
914 parent_revision_ids.push(parent_revision_id);
915 }
916
917 let id_str = effects.get_glyphs().render(Self::render_id(&id))?;
918 if parent_revision_ids.is_empty() {
919 writeln!(
920 effects.get_output_stream(),
921 "Setting {id_str} as stack root (no dependencies)",
922 )?;
923 } else {
924 writeln!(
925 effects.get_output_stream(),
926 "Stacking {id_str} on top of {}",
927 effects.get_glyphs().render(StyledStringBuilder::join(
928 ", ",
929 parent_revision_ids.iter().map(Self::render_id).collect()
930 ))?,
931 )?;
932 }
933
934 match self.set_dependencies(id, parent_revision_ids)? {
935 Ok(()) => {}
936 Err(exit_code) => return Ok(Err(exit_code)),
937 }
938 }
939 Ok(Ok(()))
940 }
941
942 fn render_id(id: &Id) -> StyledString {
943 StyledStringBuilder::new()
944 .append_styled(id.to_string(), *STYLE_PUSHED)
945 .build()
946 }
947
948 fn set_dependencies(
949 &self,
950 id: Id,
951 parent_revision_ids: Vec<Id>,
952 ) -> eyre::Result<std::result::Result<(), ExitCode>> {
953 let effects = self.effects;
954
955 if should_mock() {
956 return Ok(Ok(()));
957 }
958
959 let revisions = self.query_revisions(&DifferentialQueryRequest {
960 ids: parent_revision_ids,
961 phids: Default::default(),
962 })?;
963 let parent_revision_phids: Vec<Phid> = revisions
964 .into_iter()
965 .map(|response| response.phid)
966 .collect();
967 let request = DifferentialEditRequest {
968 id,
969 transactions: vec![DifferentialEditTransaction {
970 r#type: "parents.set".to_string(),
971 value: parent_revision_phids,
972 }],
973 };
974
975 let args = vec![
976 "call-conduit".to_string(),
977 "--".to_string(),
978 "differential.revision.edit".to_string(),
979 ];
980 let mut child = Command::new("arc")
981 .args(&args)
982 .stdin(Stdio::piped())
983 .stdout(Stdio::piped())
984 .stderr(Stdio::inherit())
985 .spawn()
986 .map_err(|err| Error::InvokeArc {
987 source: err,
988 args: args.clone(),
989 })?;
990 serde_json::to_writer_pretty(child.stdin.take().unwrap(), &request).map_err(|err| {
991 Error::CommunicateWithArc {
992 source: err,
993 args: args.clone(),
994 }
995 })?;
996 let result = child.wait_with_output().map_err(|err| Error::InvokeArc {
997 source: err,
998 args: args.clone(),
999 })?;
1000 if !result.status.success() {
1001 let args = args.join(" ");
1002 let exit_code = ExitCode::try_from(result.status)?;
1003 let ExitCode(exit_code_isize) = exit_code;
1004 writeln!(
1005 effects.get_output_stream(),
1006 "Could not update dependencies when running `arc {args}` (exit code {exit_code_isize}):",
1007 )?;
1008 writeln!(
1009 effects.get_output_stream(),
1010 "{}",
1011 String::from_utf8_lossy(&result.stdout)
1012 )?;
1013 return Ok(Err(exit_code));
1014 }
1015
1016 Ok(Ok(()))
1017 }
1018
1019 pub fn get_revision_id(&self, commit_oid: NonZeroOid) -> Result<Option<Id>> {
1022 let commit =
1023 self.repo
1024 .find_commit_or_fail(commit_oid)
1025 .map_err(|err| Error::NoSuchCommit {
1026 source: err,
1027 commit_oid,
1028 })?;
1029 let message = commit.get_message_raw();
1030
1031 lazy_static! {
1032 static ref RE: Regex = Regex::new(
1033 r"(?mx)
1034^
1035Differential[\ ]Revision:[\ ]
1036 (.+ /)?
1037 D(?P<diff>[0-9]+)
1038$",
1039 )
1040 .expect("Failed to compile `extract_diff_number` regex");
1041 }
1042 let captures = match RE.captures(message.as_slice()) {
1043 Some(captures) => captures,
1044 None => return Ok(None),
1045 };
1046 let diff_number = &captures["diff"];
1047 let diff_number = String::from_utf8(diff_number.to_vec())
1048 .expect("Regex should have confirmed that this string was only ASCII digits");
1049 Ok(Some(Id(diff_number)))
1050 }
1051
1052 fn render_failed_test(
1053 &self,
1054 commit_oid: NonZeroOid,
1055 test_output: &TestOutput,
1056 ) -> eyre::Result<()> {
1057 let commit = self.repo.find_commit_or_fail(commit_oid)?;
1058 writeln!(
1059 self.effects.get_output_stream(),
1060 "{}",
1061 self.effects
1062 .get_glyphs()
1063 .render(test_output.test_status.describe(
1064 self.effects.get_glyphs(),
1065 &commit,
1066 false
1067 )?)?,
1068 )?;
1069 let stdout = std::fs::read_to_string(&test_output.stdout_path)?;
1070 write!(self.effects.get_output_stream(), "Stdout:\n{stdout}")?;
1071 let stderr = std::fs::read_to_string(&test_output.stderr_path)?;
1072 write!(self.effects.get_output_stream(), "Stderr:\n{stderr}")?;
1073 Ok(())
1074 }
1075}