1use anyhow::{Context, Result, bail};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22#[allow(dead_code)]
23pub(crate) enum MergeMethod {
24 #[default]
25 Squash,
26 Merge,
27 Rebase,
28}
29use serde::Deserialize;
30use std::path::Path;
31use std::process::Command;
32
33use crate::runutil::{ManagedCommand, TimeoutClass, execute_managed_command};
34
35#[derive(Debug, Clone)]
36#[allow(dead_code)]
37pub(crate) struct PrInfo {
38 pub number: u32,
39 #[allow(dead_code)]
40 pub url: String,
41 #[allow(dead_code)]
42 pub head: String,
43 #[allow(dead_code)]
44 pub base: String,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub(crate) enum MergeState {
49 Clean,
50 Dirty,
51 Other(String),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub(crate) struct PrMergeStatus {
56 pub merge_state: MergeState,
57 pub is_draft: bool,
58}
59
60#[derive(Deserialize)]
61#[allow(dead_code)]
62struct PrViewJson {
63 #[serde(rename = "mergeStateStatus")]
64 merge_state_status: String,
65 number: Option<u32>,
66 url: Option<String>,
67 #[serde(rename = "headRefName")]
68 head: Option<String>,
69 #[serde(rename = "baseRefName")]
70 base: Option<String>,
71 #[serde(rename = "isDraft")]
72 is_draft: Option<bool>,
73 state: Option<String>,
74 #[serde(rename = "merged")]
75 is_merged: Option<bool>,
76 #[serde(rename = "mergedAt")]
77 merged_at: Option<String>,
78}
79
80#[derive(Deserialize)]
81#[allow(dead_code)]
82struct RepoViewNameWithOwnerJson {
83 #[serde(rename = "nameWithOwner")]
84 name_with_owner: String,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub(crate) enum PrLifecycle {
90 Open,
91 Closed,
92 Merged,
93 Unknown(String),
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub(crate) struct PrLifecycleStatus {
99 pub lifecycle: PrLifecycle,
100 pub is_merged: bool,
101}
102
103#[allow(dead_code)]
104pub(crate) fn create_pr(
105 repo_root: &Path,
106 title: &str,
107 body: &str,
108 head: &str,
109 base: &str,
110 draft: bool,
111) -> Result<PrInfo> {
112 let safe_title = title.trim();
113 if safe_title.is_empty() {
114 bail!("PR title must be non-empty");
115 }
116
117 let body = if body.trim().is_empty() {
118 "Automated by Ralph.".to_string()
119 } else {
120 body.to_string()
121 };
122
123 let mut cmd = Command::new("gh");
124 cmd.current_dir(repo_root);
125 cmd.arg("pr")
126 .arg("create")
127 .arg("--title")
128 .arg(safe_title)
129 .arg("--body")
130 .arg(body)
131 .arg("--head")
132 .arg(head)
133 .arg("--base")
134 .arg(base);
135 if draft {
136 cmd.arg("--draft");
137 }
138
139 let output = run_gh_command(cmd, "gh pr create", TimeoutClass::GitHubCli)
140 .with_context(|| format!("run gh pr create in {}", repo_root.display()))?;
141
142 if !output.status.success() {
143 let stderr = String::from_utf8_lossy(&output.stderr);
144 bail!("gh pr create failed: {}", stderr.trim());
145 }
146
147 let stdout = String::from_utf8_lossy(&output.stdout);
148 let pr_url = extract_pr_url(&stdout).ok_or_else(|| {
149 anyhow::anyhow!(
150 "Unable to parse PR URL from gh output. Output: {}",
151 stdout.trim()
152 )
153 })?;
154
155 pr_view(repo_root, &pr_url)
156}
157
158#[allow(dead_code)]
159pub(crate) fn merge_pr(
160 repo_root: &Path,
161 pr_number: u32,
162 method: MergeMethod,
163 delete_branch: bool,
164) -> Result<()> {
165 let repo_name_with_owner = gh_repo_name_with_owner(repo_root)?;
166
167 let mut cmd = Command::new("gh");
168 cmd.current_dir(std::env::temp_dir());
171 cmd.arg("pr")
172 .arg("merge")
173 .arg(pr_number.to_string())
174 .arg("--repo")
175 .arg(&repo_name_with_owner)
176 .arg(merge_method_flag(method));
177
178 if delete_branch {
179 cmd.arg("--delete-branch");
180 }
181
182 let output =
183 run_gh_command(cmd, "gh pr merge", TimeoutClass::GitHubCli).with_context(|| {
184 format!(
185 "run gh pr merge --repo {} in isolated cwd",
186 repo_name_with_owner
187 )
188 })?;
189
190 if !output.status.success() {
191 let stderr = String::from_utf8_lossy(&output.stderr);
192 bail!("gh pr merge failed: {}", stderr.trim());
193 }
194
195 Ok(())
196}
197
198#[allow(dead_code)]
199fn merge_method_flag(method: MergeMethod) -> &'static str {
200 match method {
201 MergeMethod::Squash => "--squash",
202 MergeMethod::Merge => "--merge",
203 MergeMethod::Rebase => "--rebase",
204 }
205}
206
207#[allow(dead_code)]
208fn gh_repo_name_with_owner(repo_root: &Path) -> Result<String> {
209 let mut command = Command::new("gh");
210 command
211 .current_dir(repo_root)
212 .arg("repo")
213 .arg("view")
214 .arg("--json")
215 .arg("nameWithOwner");
216 let output = run_gh_command(command, "gh repo view", TimeoutClass::GitHubCli)
217 .with_context(|| format!("run gh repo view in {}", repo_root.display()))?;
218
219 if !output.status.success() {
220 let stderr = String::from_utf8_lossy(&output.stderr);
221 bail!("gh repo view failed: {}", stderr.trim());
222 }
223
224 parse_name_with_owner_from_repo_view_json(&output.stdout)
225}
226
227#[allow(dead_code)]
228fn parse_name_with_owner_from_repo_view_json(payload: &[u8]) -> Result<String> {
229 let repo: RepoViewNameWithOwnerJson =
230 serde_json::from_slice(payload).context("parse gh repo view json")?;
231 let trimmed = repo.name_with_owner.trim();
232 if trimmed.is_empty() {
233 bail!("gh repo view returned empty nameWithOwner");
234 }
235 Ok(trimmed.to_string())
236}
237
238#[allow(dead_code)]
239pub(crate) fn pr_merge_status(repo_root: &Path, pr_number: u32) -> Result<PrMergeStatus> {
240 let json = pr_view_json(repo_root, &pr_number.to_string())?;
241 Ok(pr_merge_status_from_view(&json))
242}
243
244#[allow(dead_code)]
246pub(crate) fn pr_lifecycle_status(repo_root: &Path, pr_number: u32) -> Result<PrLifecycleStatus> {
247 let json = pr_view_json(repo_root, &pr_number.to_string())?;
248 Ok(pr_lifecycle_status_from_view(&json))
249}
250
251#[allow(dead_code)]
252fn pr_lifecycle_status_from_view(json: &PrViewJson) -> PrLifecycleStatus {
253 let state = json.state.as_deref().unwrap_or("UNKNOWN");
254 let merged_flag = json.is_merged.unwrap_or(false) || json.merged_at.as_ref().is_some();
255
256 let lifecycle = match state {
257 "OPEN" => PrLifecycle::Open,
258 "CLOSED" => {
259 if merged_flag {
260 PrLifecycle::Merged
261 } else {
262 PrLifecycle::Closed
263 }
264 }
265 "MERGED" => PrLifecycle::Merged,
266 other => PrLifecycle::Unknown(other.to_string()),
267 };
268
269 let is_merged_final = merged_flag || matches!(lifecycle, PrLifecycle::Merged);
270
271 PrLifecycleStatus {
272 lifecycle,
273 is_merged: is_merged_final,
274 }
275}
276
277#[allow(dead_code)]
278fn pr_view(repo_root: &Path, selector: &str) -> Result<PrInfo> {
279 let json = pr_view_json(repo_root, selector)?;
280 let number = json
281 .number
282 .ok_or_else(|| anyhow::anyhow!("Missing PR number in gh response"))?;
283 let url = json
284 .url
285 .ok_or_else(|| anyhow::anyhow!("Missing PR url in gh response"))?;
286 let head = json
287 .head
288 .ok_or_else(|| anyhow::anyhow!("Missing PR head in gh response"))?;
289 let base = json
290 .base
291 .ok_or_else(|| anyhow::anyhow!("Missing PR base in gh response"))?;
292
293 Ok(PrInfo {
294 number,
295 url,
296 head,
297 base,
298 })
299}
300
301#[allow(dead_code)]
302fn pr_view_json(repo_root: &Path, selector: &str) -> Result<PrViewJson> {
303 let primary_fields = "mergeStateStatus,number,url,headRefName,baseRefName,isDraft,state,merged";
304 match run_gh_pr_view(repo_root, selector, primary_fields) {
305 Ok(json) => Ok(json),
306 Err(err) => {
307 let err_msg = err.to_string();
308 if err_msg.contains("Unknown JSON field: \"merged\"") {
309 let fallback_fields =
310 "mergeStateStatus,number,url,headRefName,baseRefName,isDraft,state,mergedAt";
311 return run_gh_pr_view(repo_root, selector, fallback_fields).with_context(|| {
312 "gh pr view failed after falling back to mergedAt field".to_string()
313 });
314 }
315 Err(err)
316 }
317 }
318}
319
320#[allow(dead_code)]
321fn run_gh_pr_view(repo_root: &Path, selector: &str, fields: &str) -> Result<PrViewJson> {
322 let mut command = Command::new("gh");
323 command
324 .current_dir(repo_root)
325 .arg("pr")
326 .arg("view")
327 .arg(selector)
328 .arg("--json")
329 .arg(fields);
330 let output = run_gh_command(command, "gh pr view", TimeoutClass::GitHubCli)
331 .with_context(|| format!("run gh pr view in {}", repo_root.display()))?;
332
333 if !output.status.success() {
334 let stderr = String::from_utf8_lossy(&output.stderr);
335 bail!("gh pr view failed: {}", stderr.trim());
336 }
337
338 let json: PrViewJson =
339 serde_json::from_slice(&output.stdout).context("parse gh pr view json")?;
340 Ok(json)
341}
342
343#[allow(dead_code)]
344fn pr_merge_status_from_view(json: &PrViewJson) -> PrMergeStatus {
345 let merge_state = match json.merge_state_status.as_str() {
346 "CLEAN" => MergeState::Clean,
347 "DIRTY" => MergeState::Dirty,
348 other => MergeState::Other(other.to_string()),
349 };
350 PrMergeStatus {
351 merge_state,
352 is_draft: json.is_draft.unwrap_or(false),
353 }
354}
355
356#[allow(dead_code)]
357fn extract_pr_url(output: &str) -> Option<String> {
358 output
359 .lines()
360 .map(str::trim)
361 .find(|line| line.starts_with("http://") || line.starts_with("https://"))
362 .map(|line| line.to_string())
363}
364
365fn run_gh_with_no_update(args: &[&str]) -> Result<std::process::Output> {
367 let mut command = std::process::Command::new("gh");
368 command.args(args).env("GH_NO_UPDATE_NOTIFIER", "1");
369 run_gh_command(
370 command,
371 format!("gh {}", args.join(" ")),
372 TimeoutClass::Probe,
373 )
374 .with_context(|| format!("run gh {}", args.join(" ")))
375}
376
377pub(crate) fn check_gh_available() -> Result<()> {
385 check_gh_available_with(run_gh_with_no_update)
386}
387
388fn run_gh_command(
389 command: Command,
390 description: impl Into<String>,
391 timeout_class: TimeoutClass,
392) -> Result<std::process::Output> {
393 execute_managed_command(ManagedCommand::new(command, description, timeout_class))
394 .map(|output| {
395 let truncated = output.stdout_truncated || output.stderr_truncated;
396 if truncated {
397 log::debug!("managed gh capture truncated command output");
398 }
399 output.into_output()
400 })
401 .map_err(Into::into)
402}
403
404fn check_gh_available_with<F>(run_gh: F) -> Result<()>
406where
407 F: Fn(&[&str]) -> Result<std::process::Output>,
408{
409 let version_output = run_gh(&["--version"]).with_context(|| {
411 "GitHub CLI (`gh`) not found on PATH. Install it from https://cli.github.com/ and re-run."
412 .to_string()
413 })?;
414
415 if !version_output.status.success() {
416 let stderr = String::from_utf8_lossy(&version_output.stderr);
417 bail!(
418 "`gh --version` failed (gh is not usable). Details: {}. Install/repair `gh` from https://cli.github.com/ and re-run.",
419 stderr.trim()
420 );
421 }
422
423 let auth_output = run_gh(&["auth", "status"]).with_context(|| {
425 "Failed to run `gh auth status`. Ensure `gh` is properly installed.".to_string()
426 })?;
427
428 if !auth_output.status.success() {
429 let stdout = String::from_utf8_lossy(&auth_output.stdout);
430 let stderr = String::from_utf8_lossy(&auth_output.stderr);
431 let details = if !stderr.is_empty() {
432 stderr.trim()
433 } else {
434 stdout.trim()
435 };
436 bail!(
437 "GitHub CLI (`gh`) is not authenticated. Run `gh auth login` and re-run. Details: {}",
438 details
439 );
440 }
441
442 Ok(())
443}
444
445#[cfg(test)]
446mod tests {
447 use super::{MergeMethod, MergeState, PrLifecycle, check_gh_available_with, extract_pr_url};
448 use super::{
449 PrViewJson, merge_method_flag, parse_name_with_owner_from_repo_view_json,
450 pr_lifecycle_status_from_view, pr_merge_status_from_view,
451 };
452
453 #[test]
454 fn extract_pr_url_picks_first_url_line() {
455 let output = "Creating pull request for feature...\nhttps://github.com/org/repo/pull/5\n";
456 let url = extract_pr_url(output).expect("url");
457 assert_eq!(url, "https://github.com/org/repo/pull/5");
458 }
459
460 #[test]
461 fn pr_merge_status_from_view_tracks_draft_flag() {
462 let json = PrViewJson {
463 merge_state_status: "CLEAN".to_string(),
464 number: Some(1),
465 url: Some("https://example.com/pr/1".to_string()),
466 head: Some("ralph/RQ-0001".to_string()),
467 base: Some("main".to_string()),
468 is_draft: Some(true),
469 state: Some("OPEN".to_string()),
470 is_merged: Some(false),
471 merged_at: None,
472 };
473
474 let status = pr_merge_status_from_view(&json);
475 assert_eq!(status.merge_state, MergeState::Clean);
476 assert!(status.is_draft);
477 }
478
479 #[test]
480 fn pr_merge_status_from_view_defaults_draft_false() {
481 let json = PrViewJson {
482 merge_state_status: "DIRTY".to_string(),
483 number: Some(2),
484 url: Some("https://example.com/pr/2".to_string()),
485 head: Some("ralph/RQ-0002".to_string()),
486 base: Some("main".to_string()),
487 is_draft: None,
488 state: Some("OPEN".to_string()),
489 is_merged: Some(false),
490 merged_at: None,
491 };
492
493 let status = pr_merge_status_from_view(&json);
494 assert_eq!(status.merge_state, MergeState::Dirty);
495 assert!(!status.is_draft);
496 }
497
498 #[test]
499 fn pr_merge_status_from_view_handles_unknown_state() {
500 let json = PrViewJson {
501 merge_state_status: "BLOCKED".to_string(),
502 number: Some(3),
503 url: Some("https://example.com/pr/3".to_string()),
504 head: Some("ralph/RQ-0003".to_string()),
505 base: Some("main".to_string()),
506 is_draft: Some(false),
507 state: Some("OPEN".to_string()),
508 is_merged: Some(false),
509 merged_at: None,
510 };
511
512 let status = pr_merge_status_from_view(&json);
513 assert_eq!(status.merge_state, MergeState::Other("BLOCKED".to_string()));
514 assert!(!status.is_draft);
515 }
516
517 #[test]
518 fn pr_lifecycle_status_from_view_open() {
519 let json = PrViewJson {
520 merge_state_status: "CLEAN".to_string(),
521 number: Some(1),
522 url: Some("https://example.com/pr/1".to_string()),
523 head: Some("ralph/RQ-0001".to_string()),
524 base: Some("main".to_string()),
525 is_draft: Some(false),
526 state: Some("OPEN".to_string()),
527 is_merged: Some(false),
528 merged_at: None,
529 };
530
531 let status = pr_lifecycle_status_from_view(&json);
532 assert!(matches!(status.lifecycle, PrLifecycle::Open));
533 assert!(!status.is_merged);
534 }
535
536 #[test]
537 fn pr_lifecycle_status_from_view_closed_not_merged() {
538 let json = PrViewJson {
539 merge_state_status: "CLEAN".to_string(),
540 number: Some(2),
541 url: Some("https://example.com/pr/2".to_string()),
542 head: Some("ralph/RQ-0002".to_string()),
543 base: Some("main".to_string()),
544 is_draft: Some(false),
545 state: Some("CLOSED".to_string()),
546 is_merged: Some(false),
547 merged_at: None,
548 };
549
550 let status = pr_lifecycle_status_from_view(&json);
551 assert!(matches!(status.lifecycle, PrLifecycle::Closed));
552 assert!(!status.is_merged);
553 }
554
555 #[test]
556 fn pr_lifecycle_status_from_view_closed_merged_at() {
557 let json = PrViewJson {
558 merge_state_status: "CLEAN".to_string(),
559 number: Some(3),
560 url: Some("https://example.com/pr/3".to_string()),
561 head: Some("ralph/RQ-0003".to_string()),
562 base: Some("main".to_string()),
563 is_draft: Some(false),
564 state: Some("CLOSED".to_string()),
565 is_merged: None,
566 merged_at: Some("2026-01-19T00:00:00Z".to_string()),
567 };
568
569 let status = pr_lifecycle_status_from_view(&json);
570 assert!(matches!(status.lifecycle, PrLifecycle::Merged));
571 assert!(status.is_merged);
572 }
573
574 #[test]
575 fn pr_lifecycle_status_from_view_closed_merged() {
576 let json = PrViewJson {
577 merge_state_status: "CLEAN".to_string(),
578 number: Some(3),
579 url: Some("https://example.com/pr/3".to_string()),
580 head: Some("ralph/RQ-0003".to_string()),
581 base: Some("main".to_string()),
582 is_draft: Some(false),
583 state: Some("CLOSED".to_string()),
584 is_merged: Some(true),
585 merged_at: None,
586 };
587
588 let status = pr_lifecycle_status_from_view(&json);
589 assert!(matches!(status.lifecycle, PrLifecycle::Merged));
590 assert!(status.is_merged);
591 }
592
593 #[test]
594 fn pr_lifecycle_status_from_view_merged_state() {
595 let json = PrViewJson {
596 merge_state_status: "CLEAN".to_string(),
597 number: Some(4),
598 url: Some("https://example.com/pr/4".to_string()),
599 head: Some("ralph/RQ-0004".to_string()),
600 base: Some("main".to_string()),
601 is_draft: Some(false),
602 state: Some("MERGED".to_string()),
603 is_merged: Some(true),
604 merged_at: None,
605 };
606
607 let status = pr_lifecycle_status_from_view(&json);
608 assert!(matches!(status.lifecycle, PrLifecycle::Merged));
609 assert!(status.is_merged);
610 }
611
612 #[test]
613 fn pr_lifecycle_status_from_view_unknown_state() {
614 let json = PrViewJson {
615 merge_state_status: "CLEAN".to_string(),
616 number: Some(5),
617 url: Some("https://example.com/pr/5".to_string()),
618 head: Some("ralph/RQ-0005".to_string()),
619 base: Some("main".to_string()),
620 is_draft: Some(false),
621 state: Some("WEIRD".to_string()),
622 is_merged: Some(false),
623 merged_at: None,
624 };
625
626 let status = pr_lifecycle_status_from_view(&json);
627 assert!(matches!(status.lifecycle, PrLifecycle::Unknown(s) if s == "WEIRD"));
628 assert!(!status.is_merged);
629 }
630
631 #[test]
632 fn check_gh_available_fails_when_gh_not_found() {
633 let run_gh = |_args: &[&str]| -> anyhow::Result<std::process::Output> {
635 Err(anyhow::anyhow!(std::io::Error::new(
636 std::io::ErrorKind::NotFound,
637 "No such file or directory"
638 )))
639 };
640
641 let result = check_gh_available_with(run_gh);
642 assert!(result.is_err());
643 let msg = result.unwrap_err().to_string();
644 assert!(msg.contains("GitHub CLI (`gh`) not found on PATH"));
645 assert!(msg.contains("https://cli.github.com/"));
646 }
647
648 #[test]
649 fn check_gh_available_fails_when_version_fails() {
650 let fail_status = std::process::Command::new("false")
653 .status()
654 .expect("'false' command should exist");
655
656 let run_gh = |args: &[&str]| -> anyhow::Result<std::process::Output> {
657 if args == ["--version"] {
658 Ok(std::process::Output {
659 status: fail_status,
660 stdout: vec![],
661 stderr: b"gh: command not recognized".to_vec(),
662 })
663 } else {
664 Ok(std::process::Output {
665 status: std::process::ExitStatus::default(),
666 stdout: vec![],
667 stderr: vec![],
668 })
669 }
670 };
671
672 let result = check_gh_available_with(run_gh);
673 assert!(result.is_err());
674 let msg = result.unwrap_err().to_string();
675 assert!(msg.contains("`gh --version` failed"));
676 assert!(msg.contains("gh is not usable"));
677 }
678
679 #[test]
680 fn check_gh_available_fails_when_auth_fails() {
681 let fail_status = std::process::Command::new("false")
684 .status()
685 .expect("'false' command should exist");
686
687 let run_gh = |args: &[&str]| -> anyhow::Result<std::process::Output> {
688 if args == ["--version"] {
689 Ok(std::process::Output {
690 status: std::process::ExitStatus::default(),
691 stdout: b"gh version 2.40.0".to_vec(),
692 stderr: vec![],
693 })
694 } else if args == ["auth", "status"] {
695 Ok(std::process::Output {
696 status: fail_status,
697 stdout: vec![],
698 stderr: b"You are not logged into any GitHub hosts".to_vec(),
699 })
700 } else {
701 Ok(std::process::Output {
702 status: std::process::ExitStatus::default(),
703 stdout: vec![],
704 stderr: vec![],
705 })
706 }
707 };
708
709 let result = check_gh_available_with(run_gh);
710 assert!(result.is_err());
711 let msg = result.unwrap_err().to_string();
712 assert!(msg.contains("GitHub CLI (`gh`) is not authenticated"));
713 assert!(msg.contains("gh auth login"));
714 }
715
716 #[test]
717 fn check_gh_available_succeeds_when_both_checks_pass() {
718 let run_gh = |args: &[&str]| -> anyhow::Result<std::process::Output> {
720 if args == ["--version"] {
721 Ok(std::process::Output {
722 status: std::process::ExitStatus::default(),
723 stdout: b"gh version 2.40.0".to_vec(),
724 stderr: vec![],
725 })
726 } else if args == ["auth", "status"] {
727 Ok(std::process::Output {
728 status: std::process::ExitStatus::default(),
729 stdout: b"Logged in to github.com as user".to_vec(),
730 stderr: vec![],
731 })
732 } else {
733 Ok(std::process::Output {
734 status: std::process::ExitStatus::default(),
735 stdout: vec![],
736 stderr: vec![],
737 })
738 }
739 };
740
741 let result = check_gh_available_with(run_gh);
742 assert!(result.is_ok());
743 }
744
745 #[test]
746 fn parse_name_with_owner_from_repo_view_json_accepts_valid_payload() {
747 let payload = br#"{ "nameWithOwner": "org/repo" }"#;
748 let result = parse_name_with_owner_from_repo_view_json(payload).expect("repo");
749 assert_eq!(result, "org/repo");
750 }
751
752 #[test]
753 fn parse_name_with_owner_from_repo_view_json_rejects_empty_value() {
754 let payload = br#"{ "nameWithOwner": " " }"#;
755 let err = parse_name_with_owner_from_repo_view_json(payload).unwrap_err();
756 assert!(
757 err.to_string().contains("empty nameWithOwner"),
758 "unexpected error: {}",
759 err
760 );
761 }
762
763 #[test]
764 fn merge_method_flag_maps_all_variants() {
765 assert_eq!(merge_method_flag(MergeMethod::Squash), "--squash");
766 assert_eq!(merge_method_flag(MergeMethod::Merge), "--merge");
767 assert_eq!(merge_method_flag(MergeMethod::Rebase), "--rebase");
768 }
769}