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