use std::io::Write;
use clap::Parser;
use mkit_core::hash::Hash;
use mkit_core::layout::RepoLayout;
use mkit_core::object::{EntryMode, Object};
use mkit_core::ops::merge::find_merge_base;
use mkit_core::ops::{
DEFAULT_CONTEXT_LINES, DiffEntry, DiffKind, WhitespaceMode, detect_exact_renames, diff_trees,
unified_hunks_opts,
};
use mkit_core::refs;
use mkit_core::store::{DisplaySource, EphemeralSink, ObjectSource, ObjectStore};
use mkit_core::worktree;
use super::revspec;
use crate::clap_shim;
use crate::exit;
use crate::format;
mod stat;
pub(super) use stat::render_stat;
#[derive(Debug, Parser)]
#[command(
name = "mkit diff",
about = "Show changes as a unified patch (HEAD vs worktree, --staged, or two trees)."
)]
#[allow(clippy::struct_excessive_bools)] struct DiffOpts {
#[arg(long, visible_alias = "cached")]
staged: bool,
#[arg(long, conflicts_with = "name_status")]
name_only: bool,
#[arg(long)]
name_status: bool,
#[arg(long, conflicts_with_all = ["name_only", "name_status"])]
stat: bool,
#[arg(long = "merge-base", conflicts_with = "staged")]
merge_base: bool,
#[arg(short = 'z')]
z: bool,
#[arg(long = "exit-code")]
exit_code: bool,
#[arg(long)]
quiet: bool,
#[arg(long = "no-renames")]
no_renames: bool,
#[arg(short = 'M', long = "find-renames", value_name = "N", num_args = 0..=1, default_missing_value = "100")]
find_renames: Option<String>,
#[arg(long = "color", value_name = "WHEN", num_args = 0..=1, require_equals = true, default_missing_value = "always", conflicts_with = "no_color")]
color: Option<String>,
#[arg(long = "no-color")]
no_color: bool,
#[arg(short = 'w', long = "ignore-all-space")]
ignore_all_space: bool,
#[arg(short = 'b', long = "ignore-space-change")]
ignore_space_change: bool,
#[arg(short = 'U', long = "unified", value_name = "N")]
unified: Option<usize>,
args: Vec<String>,
}
impl DiffOpts {
fn whitespace_mode(&self) -> WhitespaceMode {
if self.ignore_all_space {
WhitespaceMode::IgnoreAllSpace
} else if self.ignore_space_change {
WhitespaceMode::IgnoreSpaceChange
} else {
WhitespaceMode::Exact
}
}
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<DiffOpts>("mkit diff", args) {
Ok(o) => o,
Err(code) => return code,
};
if opts.z && !(opts.name_only || opts.name_status) {
return emit_err(
"`-z` is only valid with `--name-only` or `--name-status`",
exit::USAGE,
);
}
let Some(color_choice) = crate::term::ColorChoice::parse(opts.color.as_deref()) else {
return emit_err("--color expects always, auto, or never", exit::USAGE);
};
let use_color = !opts.no_color
&& color_choice.resolve(std::io::IsTerminal::is_terminal(&std::io::stdout()));
let ws_mode = opts.whitespace_mode();
let context = opts.unified.unwrap_or(DEFAULT_CONTEXT_LINES);
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
};
let layout = match super::resolve_layout(&cwd) {
Ok(layout) => layout,
Err(code) => return code,
};
let store = match ObjectStore::open(&layout) {
Ok(s) => s,
Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
};
let snapshot = EphemeralSink::new(&store);
let (old_tree, new_tree, pathspecs) = match resolve_diff_endpoints(
&store,
&snapshot,
&layout,
opts.staged,
opts.merge_base,
&opts.args,
) {
Ok(v) => v,
Err((msg, code)) => return emit_err(&msg, code),
};
let mut result = match diff_trees(&snapshot, old_tree, new_tree) {
Ok(r) => r,
Err(e) => return emit_err(&format!("diff: {e}"), exit::GENERAL_ERROR),
};
if let Some(t) = &opts.find_renames {
let n = t.trim_end_matches('%');
if !n.is_empty() && n.parse::<u8>().is_err() {
return emit_err(&format!("invalid --find-renames value: {t}"), exit::USAGE);
}
}
if !opts.no_renames {
detect_exact_renames(&mut result.entries);
}
let normalized: Vec<String> = pathspecs.iter().map(|p| normalize_pathspec(p)).collect();
let selected: Vec<&mkit_core::ops::DiffEntry> = result
.entries
.iter()
.filter(|e| normalized.is_empty() || path_matches_any(&e.path, &normalized))
.collect();
let report_exit = opts.exit_code || opts.quiet;
let diff_status = if report_exit && !selected.is_empty() {
exit::GENERAL_ERROR
} else {
exit::OK
};
if opts.quiet {
return diff_status;
}
let mut stdout = std::io::stdout().lock();
if opts.stat {
return match render_stat(&mut stdout, &snapshot, selected.into_iter()) {
Ok(()) => diff_status,
Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
};
}
let display = DisplaySource::new(&snapshot);
for e in selected {
let res = if opts.name_only || opts.name_status {
emit_entry_name(&mut stdout, e, opts.name_status, opts.z);
Ok(())
} else if use_color {
let mut buf: Vec<u8> = Vec::new();
match emit_entry_patch(&mut buf, &display, e, context, ws_mode) {
Ok(()) => stdout
.write_all(&colorize_patch(&buf))
.map_err(|err| format!("write: {err}")),
Err(msg) => Err(msg),
}
} else {
emit_entry_patch(&mut stdout, &display, e, context, ws_mode)
};
if let Err(msg) = res {
return emit_err(&msg, exit::GENERAL_ERROR);
}
}
diff_status
}
fn colorize_patch(text: &[u8]) -> Vec<u8> {
const RESET: &[u8] = b"\x1b[0m";
let mut out: Vec<u8> = Vec::with_capacity(text.len() + 64);
for line in text.split_inclusive(|&b| b == b'\n') {
let (body, nl): (&[u8], &[u8]) = if line.last() == Some(&b'\n') {
(&line[..line.len() - 1], b"\n")
} else {
(line, b"")
};
let code: Option<&[u8]> = if body.starts_with(b"@@") {
Some(b"\x1b[36m") } else if body.starts_with(b"diff ")
|| body.starts_with(b"index ")
|| body.starts_with(b"new file")
|| body.starts_with(b"deleted file")
|| body.starts_with(b"old mode")
|| body.starts_with(b"new mode")
|| body.starts_with(b"rename ")
|| body.starts_with(b"similarity ")
|| body.starts_with(b"--- ")
|| body.starts_with(b"+++ ")
{
Some(b"\x1b[1m") } else if body.first() == Some(&b'+') {
Some(b"\x1b[32m") } else if body.first() == Some(&b'-') {
Some(b"\x1b[31m") } else {
None
};
match code {
Some(c) => {
out.extend_from_slice(c);
out.extend_from_slice(body);
out.extend_from_slice(RESET);
out.extend_from_slice(nl);
}
None => out.extend_from_slice(line),
}
}
out
}
fn c_quote_name(path: &str) -> String {
super::c_quote_path(path).unwrap_or_else(|| path.to_string())
}
fn name_status_letter(kind: DiffKind) -> char {
match kind {
DiffKind::Added => 'A',
DiffKind::Removed => 'D',
DiffKind::Modified => 'M',
DiffKind::ModeChanged => 'T',
DiffKind::Renamed => 'R',
}
}
fn emit_entry_name(out: &mut impl Write, e: &DiffEntry, name_status: bool, z: bool) {
if name_status && e.kind == DiffKind::Renamed {
let src = e.old_path.as_deref().unwrap_or(&e.path);
if z {
let _ = write!(out, "R100\0{src}\0{}\0", e.path);
} else {
let sq = super::c_quote_path(src).unwrap_or_else(|| src.to_string());
let dq = super::c_quote_path(&e.path).unwrap_or_else(|| e.path.clone());
let _ = writeln!(out, "R100\t{sq}\t{dq}");
}
return;
}
if z {
if name_status {
let _ = write!(out, "{}\0", name_status_letter(e.kind));
}
let _ = write!(out, "{}\0", e.path);
return;
}
let path = super::c_quote_path(&e.path);
let shown = path.as_deref().unwrap_or(&e.path);
if name_status {
let _ = writeln!(out, "{}\t{shown}", name_status_letter(e.kind));
} else {
let _ = writeln!(out, "{shown}");
}
}
type DiffEndpoints = (Option<Hash>, Option<Hash>, Vec<String>);
fn resolve_merge_base_endpoints(
store: &ObjectStore,
snapshot: &EphemeralSink<'_>,
layout: &RepoLayout,
args: &[String],
) -> Result<DiffEndpoints, (String, u8)> {
let first = args.first().ok_or_else(|| {
(
"`--merge-base` requires at least one revision".to_string(),
exit::USAGE,
)
})?;
let a = peel_tags(
store,
revspec::resolve_revision(store, layout, first)
.map_err(|e| (format!("bad revision '{first}': {e}"), exit::DATAERR))?,
);
if let Some(second) = args.get(1) {
match revspec::resolve_revision(store, layout, second) {
Ok(h) => {
let b = peel_tags(store, h);
let base = merge_base_of(store, a, b)?;
let old = object_to_tree(store, &base).map_err(|e| (e, exit::GENERAL_ERROR))?;
let new = object_to_tree(store, &b).map_err(|e| (e, exit::GENERAL_ERROR))?;
return Ok((Some(old), Some(new), args[2..].to_vec()));
}
Err(e)
if matches!(e, revspec::RevError::Unknown(_))
&& looks_like_pathspec(layout, second) => {}
Err(e) => return Err((format!("bad revision '{second}': {e}"), exit::DATAERR)),
}
}
let head = refs::resolve_head(layout)
.map_err(|e| (format!("resolve HEAD: {e}"), exit::GENERAL_ERROR))?
.ok_or_else(|| {
(
"HEAD has no commit to take a merge base with".to_string(),
exit::GENERAL_ERROR,
)
})?;
let head = peel_tags(store, head);
let base = merge_base_of(store, a, head)?;
let old = object_to_tree(store, &base).map_err(|e| (e, exit::GENERAL_ERROR))?;
let new = worktree_tree_filtered(store, snapshot, layout)?;
Ok((Some(old), Some(new), args[1..].to_vec()))
}
fn merge_base_of(store: &ObjectStore, a: Hash, b: Hash) -> Result<Hash, (String, u8)> {
find_merge_base(store, a, b)
.map_err(|e| (format!("merge base: {e}"), exit::GENERAL_ERROR))?
.ok_or_else(|| {
(
"no merge base between the given revisions".to_string(),
exit::DATAERR,
)
})
}
#[allow(clippy::too_many_arguments)]
fn resolve_diff_endpoints(
store: &ObjectStore,
snapshot: &EphemeralSink<'_>,
layout: &RepoLayout,
staged: bool,
merge_base: bool,
args: &[String],
) -> Result<DiffEndpoints, (String, u8)> {
if merge_base {
return resolve_merge_base_endpoints(store, snapshot, layout, args);
}
if staged {
if let Some(first) = args.first()
&& looks_like_rev_request(first)
{
if revspec::resolve_revision(store, layout, strip_range_end(first).0).is_ok() {
return Err((
"`--staged` diffs HEAD vs the index; it cannot take an explicit revision"
.to_string(),
exit::USAGE,
));
}
return Err((
format!("bad revision '{first}': not a known ref, commit, or short hash"),
exit::DATAERR,
));
}
let head = head_tree(store, layout).map_err(|e| (e, exit::GENERAL_ERROR))?;
let idx = index_tree(layout, store, snapshot).map_err(|e| (e, exit::GENERAL_ERROR))?;
return Ok((head, idx, args.to_vec()));
}
if let Some(first) = args.first()
&& let Some((a, b)) = split_symmetric(first)
{
let commit_a = peel_tags(
store,
revspec::resolve_revision(store, layout, a)
.map_err(|e| (format!("bad revision '{a}': {e}"), exit::DATAERR))?,
);
let commit_b = peel_tags(
store,
revspec::resolve_revision(store, layout, b)
.map_err(|e| (format!("bad revision '{b}': {e}"), exit::DATAERR))?,
);
let mb = find_merge_base(store, commit_a, commit_b)
.map_err(|e| (format!("merge base: {e}"), exit::GENERAL_ERROR))?
.ok_or_else(|| {
(
format!("no merge base between '{a}' and '{b}'"),
exit::DATAERR,
)
})?;
let old = object_to_tree(store, &mb).map_err(|e| (e, exit::GENERAL_ERROR))?;
let new = object_to_tree(store, &commit_b).map_err(|e| (e, exit::GENERAL_ERROR))?;
return Ok((Some(old), Some(new), args[1..].to_vec()));
}
if let Some(first) = args.first()
&& let Some((a, b)) = split_range(first)
{
let old = rev_to_tree(store, layout, a)?;
let new = rev_to_tree(store, layout, b)?;
return Ok((Some(old), Some(new), args[1..].to_vec()));
}
let first_rev = args.first().and_then(|a| try_rev_to_tree(store, layout, a));
match first_rev {
None => {
if let Some(first) = args.first()
&& looks_like_rev_request(first)
{
return Err((
format!("bad revision '{first}': not a known ref, commit, or short hash"),
exit::DATAERR,
));
}
let head = head_tree(store, layout).map_err(|e| (e, exit::GENERAL_ERROR))?;
let new = Some(worktree_tree_filtered(store, snapshot, layout)?);
Ok((head, new, args.to_vec()))
}
Some(Err(e)) => Err(e),
Some(Ok(old)) => {
let second_rev = args.get(1).and_then(|a| try_rev_to_tree(store, layout, a));
match second_rev {
Some(Ok(new)) => Ok((Some(old), Some(new), args[2..].to_vec())),
Some(Err(e)) => Err(e),
None => {
let new = Some(worktree_tree_filtered(store, snapshot, layout)?);
Ok((Some(old), new, args[1..].to_vec()))
}
}
}
}
}
fn worktree_tree_filtered(
store: &ObjectStore,
snapshot: &EphemeralSink<'_>,
layout: &RepoLayout,
) -> Result<Hash, (String, u8)> {
let idx =
super::read_or_seed_index_from_head(layout, store).map_err(|e| (e, exit::GENERAL_ERROR))?;
worktree::build_tree_filtered(snapshot, layout.worktree_root(), Some(&idx))
.map_err(|e| (format!("build tree: {e}"), exit::GENERAL_ERROR))
}
fn rev_to_tree(store: &ObjectStore, layout: &RepoLayout, spec: &str) -> Result<Hash, (String, u8)> {
let h = revspec::resolve_revision(store, layout, spec)
.map_err(|e| (format!("bad revision '{spec}': {e}"), exit::DATAERR))?;
object_to_tree(store, &h).map_err(|e| (e, exit::GENERAL_ERROR))
}
fn try_rev_to_tree(
store: &ObjectStore,
layout: &RepoLayout,
spec: &str,
) -> Option<Result<Hash, (String, u8)>> {
match revspec::resolve_revision(store, layout, spec) {
Ok(h) => Some(object_to_tree(store, &h).map_err(|e| (e, exit::GENERAL_ERROR))),
Err(revspec::RevError::Unknown(_)) => {
if looks_like_rev_request(spec) {
Some(Err((
format!("bad revision '{spec}': not a known ref, commit, or short hash"),
exit::DATAERR,
)))
} else {
None
}
}
Err(e) => Some(Err((format!("bad revision '{spec}': {e}"), exit::DATAERR))),
}
}
fn peel_tags(store: &ObjectStore, h: Hash) -> Hash {
super::log::peel_tags(store, h)
}
pub(super) fn object_to_tree(store: &ObjectStore, h: &Hash) -> Result<Hash, String> {
match store.read_object(h) {
Ok(Object::Commit(c)) => Ok(c.tree_hash),
Ok(Object::Remix(r)) => Ok(r.tree_hash),
Ok(Object::Tree(_)) => Ok(*h),
Ok(_) => Err(format!(
"{} is not a commit, remix, or tree",
mkit_core::hash::to_hex(h)
)),
Err(e) => Err(read_err(e)),
}
}
fn split_range(s: &str) -> Option<(&str, &str)> {
let (a, b) = s.split_once("..")?;
if a.is_empty() || b.is_empty() {
return None;
}
Some((a, b))
}
fn split_symmetric(s: &str) -> Option<(&str, &str)> {
let (a, b) = s.split_once("...")?;
Some((
if a.is_empty() { "HEAD" } else { a },
if b.is_empty() { "HEAD" } else { b },
))
}
fn strip_range_end(s: &str) -> (&str, bool) {
match s.split_once("..") {
Some((a, _)) if !a.is_empty() => (a, true),
_ => (s, false),
}
}
fn looks_like_rev_request(s: &str) -> bool {
if s.contains("..") {
return true;
}
let base = s.split(['~', '^']).next().unwrap_or(s);
if base == "HEAD" {
return true;
}
base.len() >= revspec::MIN_SHORT_HASH
&& !base.contains('/')
&& !base.contains('.')
&& base.bytes().all(|b| b.is_ascii_hexdigit())
}
fn looks_like_pathspec(layout: &RepoLayout, arg: &str) -> bool {
if layout.worktree_root().join(arg).symlink_metadata().is_ok() {
return true;
}
let spec = normalize_pathspec(arg);
let Ok(idx) = mkit_core::index::read_index(layout) else {
return false;
};
let prefix = format!("{spec}/");
idx.entries
.iter()
.any(|e| e.path == spec || e.path.starts_with(&prefix))
}
fn head_tree(store: &ObjectStore, layout: &RepoLayout) -> Result<Option<Hash>, String> {
let head = refs::resolve_head(layout).map_err(|e| format!("resolve HEAD: {e}"))?;
match head {
None => Ok(None),
Some(h) => match store.read_object(&h) {
Ok(Object::Commit(c)) => Ok(Some(c.tree_hash)),
Ok(Object::Remix(r)) => Ok(Some(r.tree_hash)),
Ok(_) => Ok(None),
Err(e) => Err(format!("read HEAD: {e}")),
},
}
}
fn index_tree(
layout: &RepoLayout,
store: &ObjectStore,
snapshot: &EphemeralSink<'_>,
) -> Result<Option<Hash>, String> {
let idx = super::read_or_seed_index_from_head(layout, store)?;
let tree = worktree::build_tree_from_index_with(store, snapshot, &idx, false)
.map_err(|e| format!("build index tree: {e}"))?;
Ok(Some(tree))
}
fn normalize_pathspec(spec: &str) -> String {
let s = spec.replace('\\', "/");
let s = s.strip_prefix("./").unwrap_or(&s);
let s = s.strip_suffix('/').unwrap_or(s);
if s == "." {
String::new()
} else {
s.to_string()
}
}
fn path_matches_any(path: &str, specs: &[String]) -> bool {
specs
.iter()
.any(|spec| spec.is_empty() || super::index_path_matches_or_descends(path, spec))
}
const ZERO_ABBREV: &str = "0000000";
fn git_octal(mode: Option<EntryMode>) -> &'static str {
match mode {
Some(EntryMode::Executable) => "100755",
Some(EntryMode::Symlink) => "120000",
Some(EntryMode::Tree) => "040000",
_ => "100644",
}
}
fn abbrev(h: Option<Hash>) -> String {
h.map_or_else(|| ZERO_ABBREV.to_string(), |h| format::short_hash(&h, 7))
}
pub(super) fn emit_entry_patch<S: ObjectSource + ?Sized>(
out: &mut impl Write,
store: &S,
e: &DiffEntry,
context: usize,
ws: WhitespaceMode,
) -> Result<(), String> {
let a_src = if e.kind == DiffKind::Renamed {
e.old_path.as_deref().unwrap_or(&e.path)
} else {
e.path.as_str()
};
let a_path = quoted_side('a', a_src);
let b_path = quoted_side('b', &e.path);
let _ = writeln!(out, "diff --git {a_path} {b_path}");
match e.kind {
DiffKind::Renamed => {
let from = super::c_quote_path(a_src).unwrap_or_else(|| a_src.to_string());
let to = super::c_quote_path(&e.path).unwrap_or_else(|| e.path.clone());
let _ = writeln!(out, "similarity index 100%");
let _ = writeln!(out, "rename from {from}");
let _ = writeln!(out, "rename to {to}");
return Ok(());
}
DiffKind::ModeChanged => {
let _ = writeln!(out, "old mode {}", git_octal(e.old_mode));
let _ = writeln!(out, "new mode {}", git_octal(e.new_mode));
return Ok(());
}
DiffKind::Added => {
let _ = writeln!(out, "new file mode {}", git_octal(e.new_mode));
let _ = writeln!(out, "index {}..{}", ZERO_ABBREV, abbrev(e.new_hash));
}
DiffKind::Removed => {
let _ = writeln!(out, "deleted file mode {}", git_octal(e.old_mode));
let _ = writeln!(out, "index {}..{}", abbrev(e.old_hash), ZERO_ABBREV);
}
DiffKind::Modified if e.old_mode != e.new_mode => {
let _ = writeln!(out, "old mode {}", git_octal(e.old_mode));
let _ = writeln!(out, "new mode {}", git_octal(e.new_mode));
let _ = writeln!(out, "index {}..{}", abbrev(e.old_hash), abbrev(e.new_hash));
}
DiffKind::Modified => {
let _ = writeln!(
out,
"index {}..{} {}",
abbrev(e.old_hash),
abbrev(e.new_hash),
git_octal(e.new_mode)
);
}
}
let old_bytes = match e.old_hash {
Some(h) => read_blob(store, &h)?,
None => Vec::new(),
};
let new_bytes = match e.new_hash {
Some(h) => read_blob(store, &h)?,
None => Vec::new(),
};
let (minus, plus) = match e.kind {
DiffKind::Added => ("/dev/null".to_string(), b_path.clone()),
DiffKind::Removed => (a_path.clone(), "/dev/null".to_string()),
_ => (a_path.clone(), b_path.clone()),
};
match unified_hunks_opts(&old_bytes, &new_bytes, context, ws) {
None => {
let _ = writeln!(out, "Binary files {minus} and {plus} differ");
}
Some(hunks) if hunks.is_empty() => {}
Some(hunks) => {
let _ = writeln!(out, "--- {minus}");
let _ = writeln!(out, "+++ {plus}");
let _ = out.write_all(&hunks);
}
}
Ok(())
}
fn quoted_side(side: char, path: &str) -> String {
let s = format!("{side}/{path}");
super::c_quote_path(&s).unwrap_or(s)
}
fn read_blob<S: ObjectSource + ?Sized>(store: &S, h: &Hash) -> Result<Vec<u8>, String> {
worktree::read_blob(store, h).map_err(read_err)
}
fn read_err<E: std::fmt::Display>(e: E) -> String {
format!("read object: {e}")
}
use super::error as emit_err;
#[cfg(test)]
mod tests {
use super::*;
fn de(path: &str, kind: DiffKind) -> DiffEntry {
DiffEntry {
path: path.to_string(),
kind,
old_hash: None,
new_hash: None,
old_mode: None,
new_mode: None,
old_path: None,
}
}
fn render(e: &DiffEntry, name_status: bool, z: bool) -> String {
let mut buf = Vec::new();
emit_entry_name(&mut buf, e, name_status, z);
String::from_utf8(buf).unwrap()
}
#[test]
fn name_status_letters_cover_every_kind() {
assert_eq!(name_status_letter(DiffKind::Added), 'A');
assert_eq!(name_status_letter(DiffKind::Removed), 'D');
assert_eq!(name_status_letter(DiffKind::Modified), 'M');
assert_eq!(name_status_letter(DiffKind::ModeChanged), 'T');
}
#[test]
fn name_only_newline_plain_path() {
assert_eq!(
render(&de("a.txt", DiffKind::Modified), false, false),
"a.txt\n"
);
}
#[test]
fn name_status_newline_is_letter_tab_path() {
assert_eq!(
render(&de("a.txt", DiffKind::Added), true, false),
"A\ta.txt\n"
);
}
#[test]
fn name_only_quotes_special_path_in_newline_mode() {
assert_eq!(
render(&de("a\tb.txt", DiffKind::Modified), false, false),
"\"a\\tb.txt\"\n"
);
}
#[test]
fn z_mode_is_raw_and_nul_terminated() {
assert_eq!(
render(&de("a\tb.txt", DiffKind::Modified), false, true),
"a\tb.txt\0"
);
assert_eq!(
render(&de("del.txt", DiffKind::Removed), true, true),
"D\0del.txt\0"
);
}
}