1use crate::config::ConfigSet;
7use crate::diff::{
8 diff_index_to_tree, diff_index_to_worktree, diff_tree_to_worktree, diff_trees, DiffEntry,
9 DiffStatus,
10};
11use crate::error::{Error, Result};
12use crate::index::Index;
13use crate::objects::ObjectId;
14use crate::odb::Odb;
15use crate::repo::Repository;
16use crate::rev_parse::{peel_to_tree, resolve_revision};
17use crate::state::resolve_head;
18use std::collections::BTreeSet;
19use std::io::{self, BufRead, Write};
20use std::path::{Path, PathBuf};
21use std::process::{Command, Stdio};
22
23#[derive(Debug, Clone, Default)]
25pub struct DifftoolEnv {
26 pub git_diff_tool: Option<String>,
28 pub git_difftool_no_prompt: bool,
30 pub git_difftool_prompt: bool,
32 pub git_mergetool_gui: Option<bool>,
34 pub display: Option<String>,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct DifftoolOptions {
41 pub gui: Option<bool>,
43 pub dir_diff: bool,
45 pub prompt: Option<bool>,
47 pub trust_exit_code: bool,
49 pub no_trust_exit_code: bool,
51 pub tool: Option<String>,
53 pub extcmd: Option<String>,
55 pub tool_help: bool,
57 pub no_index: bool,
59 pub symlinks: Option<bool>,
61 pub rotate_to: Option<String>,
63 pub skip_to: Option<String>,
65 pub diff_argv: Vec<String>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct DifftoolResult {
72 pub exit_code: i32,
74}
75
76pub fn parse_difftool_argv(argv: &[String]) -> Result<DifftoolOptions> {
80 let mut opts = DifftoolOptions::default();
81 let mut i = 0;
82 while i < argv.len() {
83 let arg = &argv[i];
84 match arg.as_str() {
85 "-g" | "--gui" => {
86 opts.gui = Some(true);
87 }
88 "--no-gui" => {
89 opts.gui = Some(false);
90 }
91 "-d" | "--dir-diff" => {
92 opts.dir_diff = true;
93 }
94 "-y" | "--no-prompt" => {
95 opts.prompt = Some(false);
96 }
97 "--prompt" => {
98 opts.prompt = Some(true);
99 }
100 "--trust-exit-code" => {
101 opts.trust_exit_code = true;
102 }
103 "--no-trust-exit-code" => {
104 opts.no_trust_exit_code = true;
105 }
106 "--tool-help" => {
107 opts.tool_help = true;
108 }
109 "--no-index" => {
110 opts.no_index = true;
111 opts.diff_argv.push(arg.clone());
112 }
113 "--symlinks" => {
114 opts.symlinks = Some(true);
115 }
116 "--no-symlinks" => {
117 opts.symlinks = Some(false);
118 }
119 "-t" | "--tool" => {
120 i += 1;
121 let val = argv
122 .get(i)
123 .ok_or_else(|| Error::Message("option '--tool' requires an argument".into()))?;
124 opts.tool = Some(parse_tool_value(val)?);
125 }
126 "-x" | "--extcmd" => {
127 i += 1;
128 let val = argv.get(i).ok_or_else(|| {
129 Error::Message("option '--extcmd' requires an argument".into())
130 })?;
131 opts.extcmd = Some(val.clone());
132 }
133 s if s.starts_with("--tool=") => {
134 opts.tool = Some(parse_tool_value(s.strip_prefix("--tool=").unwrap_or(""))?);
135 }
136 s if s.starts_with("--extcmd=") => {
137 opts.extcmd = Some(s.strip_prefix("--extcmd=").unwrap_or("").to_string());
138 }
139 s if s.starts_with("--rotate-to=") => {
140 opts.rotate_to = Some(s.strip_prefix("--rotate-to=").unwrap_or("").to_string());
141 }
142 s if s.starts_with("--skip-to=") => {
143 opts.skip_to = Some(s.strip_prefix("--skip-to=").unwrap_or("").to_string());
144 }
145 "--" => {
146 opts.diff_argv.push("--".to_string());
147 opts.diff_argv.extend_from_slice(&argv[i + 1..]);
148 break;
149 }
150 _ if arg.starts_with('-') => {
151 opts.diff_argv.push(arg.clone());
152 }
153 _ => {
154 opts.diff_argv.push(arg.clone());
155 }
156 }
157 i += 1;
158 }
159 Ok(opts)
160}
161
162fn parse_tool_value(raw: &str) -> Result<String> {
163 if raw.is_empty() {
164 return Err(Error::Message("no <tool> given for --tool=<tool>".into()));
165 }
166 Ok(raw.to_string())
167}
168
169pub fn print_tool_help(config: &ConfigSet, stdout: &mut dyn Write) -> io::Result<()> {
171 writeln!(
172 stdout,
173 "'git difftool --tool=<tool>' may be set to one of the following:"
174 )?;
175 writeln!(stdout)?;
176 let mut names = BTreeSet::new();
177 for entry in config.entries() {
178 if let Some(rest) = entry.key.strip_prefix("difftool.") {
179 if let Some(tool) = rest.strip_suffix(".cmd") {
180 names.insert(tool.to_string());
181 }
182 }
183 if let Some(rest) = entry.key.strip_prefix("mergetool.") {
184 if let Some(tool) = rest.strip_suffix(".cmd") {
185 names.insert(tool.to_string());
186 }
187 }
188 }
189 for tool in &names {
190 writeln!(stdout, "\t{tool:<15}")?;
191 }
192 for tool in ["vimdiff", "meld", "kompare", "tkdiff"] {
193 if !names.contains(tool) {
194 writeln!(stdout, "\t{tool:<15}")?;
195 }
196 }
197 writeln!(stdout)?;
198 Ok(())
199}
200
201pub fn run_difftool(
203 repo: Option<&Repository>,
204 opts: &DifftoolOptions,
205 env: &DifftoolEnv,
206 config: &ConfigSet,
207 stdin: &mut dyn BufRead,
208 stdout: &mut dyn Write,
209) -> Result<DifftoolResult> {
210 if opts.tool_help {
211 print_tool_help(config, stdout)?;
212 return Ok(DifftoolResult { exit_code: 0 });
213 }
214
215 if opts.no_index {
216 return run_no_index_difftool(opts, env, config, stdin, stdout);
217 }
218
219 let repo = repo.ok_or_else(|| Error::NotARepository(".".into()))?;
220 let work_tree = repo
221 .work_tree
222 .as_deref()
223 .ok_or_else(|| Error::Message("this operation must be run in a work tree".into()))?;
224
225 if opts.gui.is_some() && opts.tool.is_some() {
226 return Err(Error::Message(
227 "options '--gui' and '--tool' cannot be used together".into(),
228 ));
229 }
230 if opts.gui.is_some() && opts.extcmd.is_some() {
231 return Err(Error::Message(
232 "options '--gui' and '--extcmd' cannot be used together".into(),
233 ));
234 }
235 if opts.tool.is_some() && opts.extcmd.is_some() {
236 return Err(Error::Message(
237 "options '--tool' and '--extcmd' cannot be used together".into(),
238 ));
239 }
240
241 let trust_exit_code = resolve_trust_exit_code(opts, config);
242 let should_prompt = resolve_should_prompt(opts, env, config);
243 let tool_ctx = resolve_tool_context(opts, env, config)?;
244
245 let index = match repo.load_index() {
246 Ok(idx) => idx,
247 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Index::new(),
248 Err(e) => return Err(e),
249 };
250
251 let mut entries = collect_diff_entries(repo, &index, work_tree, &opts.diff_argv)?;
252 entries = apply_rotate_skip(entries, opts.rotate_to.as_deref(), opts.skip_to.as_deref())?;
253
254 if entries.is_empty() {
255 return Ok(DifftoolResult { exit_code: 0 });
256 }
257
258 if opts.dir_diff {
259 return run_dir_diff(
260 repo,
261 &entries,
262 work_tree,
263 &index,
264 &tool_ctx,
265 opts,
266 env,
267 config,
268 trust_exit_code,
269 should_prompt,
270 stdin,
271 stdout,
272 );
273 }
274
275 let tmp_dir = tempfile::tempdir().map_err(Error::Io)?;
276 let total = entries.len();
277 for (idx, entry) in entries.iter().enumerate() {
278 let counter = idx + 1;
279 let exit = launch_file_diff(
280 repo,
281 entry,
282 work_tree,
283 tmp_dir.path(),
284 &tool_ctx,
285 counter,
286 total,
287 should_prompt,
288 trust_exit_code,
289 stdin,
290 stdout,
291 )?;
292 if exit != 0 && trust_exit_code {
293 return Ok(DifftoolResult { exit_code: exit });
294 }
295 if exit >= 126 {
296 return Ok(DifftoolResult { exit_code: exit });
297 }
298 }
299 Ok(DifftoolResult { exit_code: 0 })
300}
301
302#[derive(Debug, Clone)]
304struct ToolContext {
305 tool_name: String,
306 extcmd: Option<String>,
307 tool_cmd: Option<String>,
308 tool_path: Option<String>,
309}
310
311fn resolve_trust_exit_code(opts: &DifftoolOptions, config: &ConfigSet) -> bool {
312 if opts.no_trust_exit_code {
313 return false;
314 }
315 if opts.trust_exit_code {
316 return true;
317 }
318 config
319 .get_bool("difftool.trustExitCode")
320 .and_then(|r| r.ok())
321 .unwrap_or(false)
322}
323
324fn resolve_should_prompt(opts: &DifftoolOptions, env: &DifftoolEnv, config: &ConfigSet) -> bool {
325 if env.git_difftool_no_prompt {
326 return false;
327 }
328 if env.git_difftool_prompt {
329 return true;
330 }
331 if let Some(p) = opts.prompt {
332 return p;
333 }
334 let prompt_merge = config
335 .get_bool("mergetool.prompt")
336 .and_then(|r| r.ok())
337 .unwrap_or(true);
338 config
339 .get_bool("difftool.prompt")
340 .and_then(|r| r.ok())
341 .unwrap_or(prompt_merge)
342}
343
344fn gui_default(config: &ConfigSet, env: &DifftoolEnv) -> Result<bool> {
345 let raw = config
346 .get("difftool.guiDefault")
347 .map(|s| s.to_ascii_lowercase())
348 .unwrap_or_else(|| "false".to_string());
349 if raw == "auto" {
350 return Ok(env.display.as_ref().is_some_and(|d| !d.is_empty()));
351 }
352 Ok(config
353 .get_bool("difftool.guiDefault")
354 .and_then(|r| r.ok())
355 .unwrap_or(false))
356}
357
358fn resolve_tool_context(
359 opts: &DifftoolOptions,
360 env: &DifftoolEnv,
361 config: &ConfigSet,
362) -> Result<ToolContext> {
363 if let Some(ext) = &opts.extcmd {
364 return Ok(ToolContext {
365 tool_name: ext.clone(),
366 extcmd: Some(ext.clone()),
367 tool_cmd: None,
368 tool_path: None,
369 });
370 }
371
372 let use_gui = match opts.gui {
373 Some(v) => v,
374 None => match env.git_mergetool_gui {
375 Some(v) => v,
376 None => gui_default(config, env)?,
377 },
378 };
379
380 let tool_name = if let Some(t) = opts.tool.clone().or_else(|| env.git_diff_tool.clone()) {
381 t
382 } else {
383 select_configured_tool(config, use_gui)?
384 };
385
386 if !valid_tool(config, &tool_name) {
387 return Err(Error::Message(format!("Unknown diff tool {tool_name}")));
388 }
389
390 let tool_cmd = get_tool_cmd(config, &tool_name);
391 let path_key = format!("difftool.{tool_name}.path");
392 let merge_path_key = format!("mergetool.{tool_name}.path");
393 let tool_path = config
394 .get(&path_key)
395 .or_else(|| config.get(&merge_path_key))
396 .or_else(|| Some(tool_name.clone()));
397
398 Ok(ToolContext {
399 tool_name,
400 extcmd: None,
401 tool_cmd,
402 tool_path,
403 })
404}
405
406fn select_configured_tool(config: &ConfigSet, use_gui: bool) -> Result<String> {
407 let keys: &[&str] = if use_gui {
408 &["diff.guitool", "merge.guitool", "diff.tool", "merge.tool"]
409 } else {
410 &["diff.tool", "merge.tool"]
411 };
412 for key in keys {
413 if let Some(val) = config.get(key).filter(|s| !s.is_empty()) {
414 if valid_tool(config, &val) {
415 return Ok(val);
416 }
417 }
418 }
419 Ok("vimdiff".to_string())
420}
421
422fn get_tool_cmd(config: &ConfigSet, tool: &str) -> Option<String> {
423 config
424 .get(&format!("difftool.{tool}.cmd"))
425 .or_else(|| config.get(&format!("mergetool.{tool}.cmd")))
426}
427
428fn valid_tool(config: &ConfigSet, tool: &str) -> bool {
429 if get_tool_cmd(config, tool).is_some() {
430 return true;
431 }
432 let path_key = format!("difftool.{tool}.path");
433 let merge_path_key = format!("mergetool.{tool}.path");
434 if let Some(path) = config
435 .get(&path_key)
436 .or_else(|| config.get(&merge_path_key))
437 {
438 if Command::new("sh")
439 .arg("-c")
440 .arg(format!("type {} >/dev/null 2>&1", shell_quote(&path)))
441 .status()
442 .ok()
443 .is_some_and(|s| s.success())
444 {
445 return true;
446 }
447 }
448 which_tool_executable(tool).is_some()
449}
450
451fn which_tool_executable(tool: &str) -> Option<String> {
452 if Command::new("sh")
453 .arg("-c")
454 .arg(format!("type {tool} >/dev/null 2>&1"))
455 .status()
456 .ok()
457 .is_some_and(|s| s.success())
458 {
459 return Some(tool.to_string());
460 }
461 None
462}
463
464fn collect_diff_entries(
465 repo: &Repository,
466 index: &Index,
467 work_tree: &Path,
468 diff_argv: &[String],
469) -> Result<Vec<DiffEntry>> {
470 let mut cached = false;
471 let mut revs = Vec::new();
472 let mut paths = Vec::new();
473 let mut in_paths = false;
474 for arg in diff_argv {
475 if in_paths {
476 paths.push(arg.clone());
477 continue;
478 }
479 if arg == "--" {
480 in_paths = true;
481 continue;
482 }
483 match arg.as_str() {
484 "--cached" | "--staged" => cached = true,
485 _ if arg.starts_with('-') => {}
486 _ => revs.push(arg.clone()),
487 }
488 }
489
490 let head_tree = head_tree_oid(repo).ok();
491 let entries = match (cached, revs.len()) {
492 (true, 0) => diff_index_to_tree(&repo.odb, index, head_tree.as_ref(), false)?,
493 (true, 1) => {
494 let tree = commit_or_tree_oid(repo, &revs[0])?;
495 diff_index_to_tree(&repo.odb, index, Some(&tree), false)?
496 }
497 (false, 0) => diff_index_to_worktree(&repo.odb, index, work_tree, false, false)?,
498 (false, 1) => {
499 let tree = commit_or_tree_oid(repo, &revs[0])?;
500 diff_tree_to_worktree(&repo.odb, Some(&tree), work_tree, index)?
501 }
502 (false, 2) => {
503 let t1 = commit_or_tree_oid(repo, &revs[0])?;
504 let t2 = commit_or_tree_oid(repo, &revs[1])?;
505 diff_trees(&repo.odb, Some(&t1), Some(&t2), "")?
506 }
507 _ => {
508 return Err(Error::Message("too many revisions for difftool".into()));
509 }
510 };
511
512 Ok(filter_paths(entries, &paths))
513}
514
515fn filter_paths(entries: Vec<DiffEntry>, paths: &[String]) -> Vec<DiffEntry> {
516 if paths.is_empty() {
517 return entries;
518 }
519 entries
520 .into_iter()
521 .filter(|e| {
522 let p = e.path();
523 paths
524 .iter()
525 .any(|f| p == f || p.starts_with(&format!("{f}/")))
526 })
527 .collect()
528}
529
530fn apply_rotate_skip(
531 mut entries: Vec<DiffEntry>,
532 rotate_to: Option<&str>,
533 skip_to: Option<&str>,
534) -> Result<Vec<DiffEntry>> {
535 if let Some(target) = rotate_to {
536 let pos = entries
537 .iter()
538 .position(|e| e.path() == target)
539 .ok_or_else(|| Error::Message(format!("File '{target}' not in diff list")))?;
540 let tail = entries.split_off(pos);
541 entries = tail;
542 }
543 if let Some(target) = skip_to {
544 let pos = entries
545 .iter()
546 .position(|e| e.path() == target)
547 .ok_or_else(|| Error::Message(format!("File '{target}' not in diff list")))?;
548 entries = entries.split_off(pos);
549 }
550 Ok(entries)
551}
552
553fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
554 let head = resolve_head(&repo.git_dir)?;
555 let Some(oid) = head.oid() else {
556 return Err(Error::Message("unborn HEAD".into()));
557 };
558 peel_to_tree(repo, *oid)
559}
560
561fn commit_or_tree_oid(repo: &Repository, spec: &str) -> Result<ObjectId> {
562 let oid = resolve_revision(repo, spec).map_err(|e| Error::Message(e.to_string()))?;
563 peel_to_tree(repo, oid)
564}
565
566fn launch_file_diff(
567 repo: &Repository,
568 entry: &DiffEntry,
569 work_tree: &Path,
570 tmp_dir: &Path,
571 tool: &ToolContext,
572 counter: usize,
573 total: usize,
574 should_prompt: bool,
575 trust_exit_code: bool,
576 stdin: &mut dyn BufRead,
577 stdout: &mut dyn Write,
578) -> Result<i32> {
579 let merged = entry.path();
580 let (local_path, remote_path) = materialize_pair(repo, entry, work_tree, tmp_dir)?;
581
582 if should_prompt {
583 writeln!(stdout)?;
584 writeln!(stdout, "Viewing ({counter}/{total}): '{merged}'")?;
585 let prompt_label = tool.extcmd.as_deref().unwrap_or(&tool.tool_name);
586 write!(stdout, "Launch '{prompt_label}' [Y/n]? ")?;
587 stdout.flush().map_err(Error::Io)?;
588 let mut line = String::new();
589 if stdin.read_line(&mut line).ok().filter(|n| *n > 0).is_none() {
590 return Ok(0);
591 }
592 let ans = line.trim();
593 if ans.eq_ignore_ascii_case("n") || ans.eq_ignore_ascii_case("no") {
594 return Ok(0);
595 }
596 }
597
598 let status = run_tool(tool, &local_path, &remote_path, merged, counter, total)?;
599 let mut code = status.code().unwrap_or(1);
600 if code == 127 {
601 code = 128;
602 }
603 if trust_exit_code && code != 0 {
604 return Ok(code);
605 }
606 if code >= 126 {
607 return Ok(code);
608 }
609 Ok(0)
610}
611
612fn materialize_pair(
613 repo: &Repository,
614 entry: &DiffEntry,
615 work_tree: &Path,
616 tmp_dir: &Path,
617) -> Result<(PathBuf, PathBuf)> {
618 let safe_name = entry.path().replace('/', "_");
619 let local_tmp = tmp_dir.join(format!("local_{safe_name}"));
620 let remote_tmp = tmp_dir.join(format!("remote_{safe_name}"));
621
622 match entry.status {
623 DiffStatus::Added => {
624 write_blob_or_empty(&repo.odb, &entry.new_oid, &local_tmp)?;
625 let wt = work_tree.join(entry.path());
626 Ok((local_tmp, wt))
627 }
628 DiffStatus::Deleted => {
629 write_blob_or_empty(&repo.odb, &entry.old_oid, &local_tmp)?;
630 Ok((local_tmp, PathBuf::from("/dev/null")))
631 }
632 _ => {
633 write_blob_or_empty(&repo.odb, &entry.old_oid, &local_tmp)?;
634 let wt = work_tree.join(entry.path());
635 if wt.exists() {
636 Ok((local_tmp, wt))
637 } else {
638 write_blob_or_empty(&repo.odb, &entry.new_oid, &remote_tmp)?;
639 Ok((local_tmp, remote_tmp))
640 }
641 }
642 }
643}
644
645fn write_blob_or_empty(odb: &Odb, oid: &ObjectId, dest: &Path) -> Result<()> {
646 if oid.is_zero() {
647 std::fs::write(dest, "").map_err(Error::Io)?;
648 return Ok(());
649 }
650 let data = odb.read(oid)?;
651 std::fs::write(dest, &data.data).map_err(Error::Io)?;
652 Ok(())
653}
654
655fn run_tool(
656 tool: &ToolContext,
657 local: &Path,
658 remote: &Path,
659 merged: &str,
660 counter: usize,
661 total: usize,
662) -> Result<std::process::ExitStatus> {
663 if let Some(extcmd) = &tool.extcmd {
664 let script = format!(
665 "export LOCAL={local} REMOTE={remote} MERGED={merged} BASE={merged}; \
666 export GIT_DIFF_PATH_COUNTER={counter} GIT_DIFF_PATH_TOTAL={total} GIT_PREFIX=.; \
667 set -- \"$MERGED\" \"$LOCAL\" \"$REMOTE\"; \
668 eval {extcmd} \"$LOCAL\" \"$REMOTE\"",
669 local = shell_quote(&local.display().to_string()),
670 remote = shell_quote(&remote.display().to_string()),
671 merged = shell_quote(merged),
672 extcmd = extcmd,
673 );
674 return Command::new("sh")
675 .arg("-c")
676 .arg(&script)
677 .stdout(Stdio::inherit())
678 .status()
679 .map_err(Error::Io);
680 }
681
682 if let Some(tool_cmd) = &tool.tool_cmd {
683 let script = format!(
684 "export LOCAL={local} REMOTE={remote} MERGED={merged} BASE={merged}; \
685 export GIT_DIFF_PATH_COUNTER={counter} GIT_DIFF_PATH_TOTAL={total} GIT_PREFIX=.; \
686 export merge_tool={name} merge_tool_path={path}; \
687 eval {tool_cmd}",
688 local = shell_quote(&local.display().to_string()),
689 remote = shell_quote(&remote.display().to_string()),
690 merged = shell_quote(merged),
691 name = shell_quote(&tool.tool_name),
692 path = shell_quote(tool.tool_path.as_deref().unwrap_or(&tool.tool_name)),
693 tool_cmd = tool_cmd,
694 );
695 return Command::new("sh")
696 .arg("-c")
697 .arg(&script)
698 .stdout(Stdio::inherit())
699 .status()
700 .map_err(Error::Io);
701 }
702
703 let exe = tool.tool_path.as_deref().unwrap_or(&tool.tool_name);
704 Command::new(exe)
705 .arg(local)
706 .arg(remote)
707 .stdout(Stdio::inherit())
708 .status()
709 .map_err(Error::Io)
710}
711
712fn shell_quote(s: &str) -> String {
713 if s.is_empty() {
714 return "''".to_string();
715 }
716 if s.chars()
717 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '@' | '%' | '+' | '-' | '_' | '.' | '/'))
718 {
719 return s.to_string();
720 }
721 format!("'{}'", s.replace('\'', "'\\''"))
722}
723
724fn run_dir_diff(
725 repo: &Repository,
726 entries: &[DiffEntry],
727 work_tree: &Path,
728 index: &Index,
729 tool: &ToolContext,
730 opts: &DifftoolOptions,
731 _env: &DifftoolEnv,
732 config: &ConfigSet,
733 trust_exit_code: bool,
734 should_prompt: bool,
735 stdin: &mut dyn BufRead,
736 stdout: &mut dyn Write,
737) -> Result<DifftoolResult> {
738 let tmp = tempfile::tempdir().map_err(Error::Io)?;
739 let left = tmp.path().join("left");
740 let right = tmp.path().join("right");
741 std::fs::create_dir_all(&left).map_err(Error::Io)?;
742 std::fs::create_dir_all(&right).map_err(Error::Io)?;
743
744 let use_symlinks = opts
745 .symlinks
746 .or_else(|| config.get_bool("core.symlinks").and_then(|r| r.ok()))
747 .unwrap_or(true);
748
749 for entry in entries {
750 populate_dir_side(repo, &left, entry, true, work_tree, index, use_symlinks)?;
751 populate_dir_side(repo, &right, entry, false, work_tree, index, use_symlinks)?;
752 }
753
754 if should_prompt {
755 let prompt_label = tool.extcmd.as_deref().unwrap_or(&tool.tool_name);
756 write!(stdout, "Launch '{prompt_label}' [Y/n]? ")?;
757 stdout.flush().map_err(Error::Io)?;
758 let mut line = String::new();
759 if stdin.read_line(&mut line).ok().filter(|n| *n > 0).is_none() {
760 return Ok(DifftoolResult { exit_code: 0 });
761 }
762 let ans = line.trim();
763 if ans.eq_ignore_ascii_case("n") || ans.eq_ignore_ascii_case("no") {
764 return Ok(DifftoolResult { exit_code: 0 });
765 }
766 }
767
768 let status = if let Some(extcmd) = &tool.extcmd {
769 let script = format!(
770 "export LOCAL={} REMOTE={}; export GIT_DIFFTOOL_DIRDIFF=true; \
771 set -- . \"$LOCAL\" \"$REMOTE\"; eval {} \"$LOCAL\" \"$REMOTE\"",
772 shell_quote(&left.display().to_string()),
773 shell_quote(&right.display().to_string()),
774 extcmd,
775 );
776 Command::new("sh")
777 .arg("-c")
778 .arg(script)
779 .stdout(Stdio::inherit())
780 .status()
781 .map_err(Error::Io)?
782 } else if let Some(tool_cmd) = &tool.tool_cmd {
783 let script = format!(
784 "export LOCAL={} REMOTE={} MERGED=. BASE=.; export GIT_DIFFTOOL_DIRDIFF=true; \
785 export merge_tool={} merge_tool_path={}; eval {}",
786 shell_quote(&left.display().to_string()),
787 shell_quote(&right.display().to_string()),
788 shell_quote(&tool.tool_name),
789 shell_quote(tool.tool_path.as_deref().unwrap_or(&tool.tool_name)),
790 tool_cmd,
791 );
792 Command::new("sh")
793 .arg("-c")
794 .arg(script)
795 .stdout(Stdio::inherit())
796 .status()
797 .map_err(Error::Io)?
798 } else {
799 let exe = tool.tool_path.as_deref().unwrap_or(&tool.tool_name);
800 Command::new(exe)
801 .arg(&left)
802 .arg(&right)
803 .stdout(Stdio::inherit())
804 .status()
805 .map_err(Error::Io)?
806 };
807
808 let code = status.code().unwrap_or(1);
809 if code >= 126 {
810 return Ok(DifftoolResult { exit_code: code });
811 }
812 if trust_exit_code && code != 0 {
813 return Ok(DifftoolResult { exit_code: code });
814 }
815 Ok(DifftoolResult { exit_code: 0 })
816}
817
818fn populate_dir_side(
819 repo: &Repository,
820 dir: &Path,
821 entry: &DiffEntry,
822 is_left: bool,
823 work_tree: &Path,
824 index: &Index,
825 use_symlinks: bool,
826) -> Result<()> {
827 let path = if is_left {
828 entry.old_path.as_deref().or(entry.new_path.as_deref())
829 } else {
830 entry.new_path.as_deref().or(entry.old_path.as_deref())
831 };
832 let Some(rel) = path else {
833 return Ok(());
834 };
835 let dest = dir.join(rel);
836 if let Some(parent) = dest.parent() {
837 std::fs::create_dir_all(parent).map_err(Error::Io)?;
838 }
839
840 let mode_str = if is_left {
841 &entry.old_mode
842 } else {
843 &entry.new_mode
844 };
845 let oid = if is_left {
846 &entry.old_oid
847 } else {
848 &entry.new_oid
849 };
850
851 if mode_str == "160000" {
852 let label = if oid.is_zero() {
853 "Subproject commit 0000000000000000000000000000000000000000"
854 } else {
855 &format!("Subproject commit {}", oid.to_hex())
856 };
857 std::fs::write(&dest, label).map_err(Error::Io)?;
858 return Ok(());
859 }
860
861 if mode_str.starts_with("120000") {
862 let target = if oid.is_zero() {
863 std::fs::read_link(work_tree.join(rel))
864 .map(|p| p.to_string_lossy().into_owned())
865 .unwrap_or_default()
866 } else {
867 String::from_utf8_lossy(&repo.odb.read(oid)?.data).into_owned()
868 };
869 if use_symlinks {
870 let _ = std::fs::remove_file(&dest);
871 std::os::unix::fs::symlink(&target, &dest).map_err(Error::Io)?;
872 } else {
873 std::fs::write(&dest, target).map_err(Error::Io)?;
874 }
875 return Ok(());
876 }
877
878 if oid.is_zero() {
879 return Ok(());
880 }
881
882 let data = repo.odb.read(oid)?;
883 std::fs::write(&dest, &data.data).map_err(Error::Io)?;
884
885 if !is_left {
887 let wt = work_tree.join(rel);
888 if wt.exists() {
889 if let Ok(bytes) = std::fs::read(&wt) {
890 std::fs::write(&dest, bytes).map_err(Error::Io)?;
891 }
892 } else if let Some(idx) = index.get(rel.as_bytes(), 0) {
893 if !idx.oid.is_zero() {
894 let data = repo.odb.read(&idx.oid)?;
895 std::fs::write(&dest, &data.data).map_err(Error::Io)?;
896 }
897 }
898 }
899 Ok(())
900}
901
902fn run_no_index_difftool(
903 opts: &DifftoolOptions,
904 env: &DifftoolEnv,
905 config: &ConfigSet,
906 stdin: &mut dyn BufRead,
907 stdout: &mut dyn Write,
908) -> Result<DifftoolResult> {
909 let mut paths = Vec::new();
910 let mut seen_no_index = false;
911 for arg in &opts.diff_argv {
912 if arg == "--no-index" {
913 seen_no_index = true;
914 continue;
915 }
916 if !arg.starts_with('-') {
917 paths.push(arg.clone());
918 }
919 }
920 if !seen_no_index || paths.len() != 2 {
921 return Err(Error::Message(
922 "difftool --no-index requires exactly two paths".into(),
923 ));
924 }
925 let tool_ctx = resolve_tool_context(opts, env, config)?;
926 let local = PathBuf::from(&paths[0]);
927 let remote = PathBuf::from(&paths[1]);
928 let should_prompt = resolve_should_prompt(opts, env, config);
929 if should_prompt {
930 write!(stdout, "Launch '{}' [Y/n]? ", tool_ctx.tool_name)?;
931 stdout.flush().map_err(Error::Io)?;
932 let mut line = String::new();
933 if stdin.read_line(&mut line).ok().filter(|n| *n > 0).is_none() {
934 return Ok(DifftoolResult { exit_code: 0 });
935 }
936 }
937 let status = run_tool(
938 &tool_ctx,
939 &local,
940 &remote,
941 local.file_name().and_then(|s| s.to_str()).unwrap_or(""),
942 1,
943 1,
944 )?;
945 Ok(DifftoolResult {
946 exit_code: status.code().unwrap_or(1),
947 })
948}