use std::path::Path;
use clap::Args;
use tedi::{
Issue, IssueIndex, IssueLink, LazyIssue, RepoInfo, github,
local::{Consensus, ExactMatchLevel, FsReader, Local, LocalFs, LocalIssueSource, LocalPath},
remote::RemoteSource,
sink::Sink,
};
use v_utils::prelude::*;
use super::{
sync::{MergeMode, Modifier, Side, SyncOptions, modify_and_sync_issue},
touch::parse_touch_path,
};
use crate::{MockType, config::LiveSettings};
#[derive(Args, Debug)]
pub struct OpenArgs {
pub url_or_pattern: Option<String>,
#[arg(short = 'e', long, action = clap::ArgAction::Count)]
pub exact: u8,
#[arg(short, long)]
pub touch: bool,
#[arg(short, long)]
pub last: bool,
#[arg(long)] pub pull: bool,
#[arg(short, long)]
pub blocker: bool,
#[arg(long)]
pub blocker_set: bool,
#[arg(short, long)]
pub force: bool,
#[arg(short, long)]
pub reset: bool,
#[arg(long, value_name = "TYPE", default_missing_value = "default", num_args = 0..=1)]
pub parent: Option<ProjectType>, }
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, clap::ValueEnum)]
pub enum ProjectType {
#[default]
#[value(name = "default")]
Default,
Virtual,
}
#[tracing::instrument(level = "debug", skip_all, fields(
url_or_pattern = ?args.url_or_pattern,
touch = args.touch,
blocker = args.blocker,
force = args.force,
reset = args.reset,
offline,
mock = ?mock,
))]
pub async fn open_command(settings: &LiveSettings, args: OpenArgs, offline: bool, mock: Option<MockType>) -> Result<()> {
tracing::debug!("open_command entered, blocker={}", args.blocker);
let make_modifier = |open_at_blocker: bool| -> Modifier {
match mock {
Some(MockType::GhostEdit) => Modifier::MockGhostEdit,
_ => Modifier::Editor { open_at_blocker },
}
};
let _ = settings;
let exact = ExactMatchLevel::try_from(args.exact).map_err(|e| eyre!(e))?;
let build_merge_mode = |prefer: Side| -> Option<MergeMode> {
if args.reset {
Some(MergeMode::Reset { prefer })
} else if args.force {
Some(MergeMode::Force { prefer })
} else {
None
}
};
let make_sync_opts = |prefer_remote: bool| {
let prefer = if prefer_remote { Side::Remote } else { Side::Local };
SyncOptions::new(build_merge_mode(prefer), prefer_remote || args.pull)
};
let local_sync_opts = || make_sync_opts(args.pull);
let remote_sync_opts = || make_sync_opts(true);
let open_at_blocker = args.blocker || args.blocker_set;
let input = if open_at_blocker && args.url_or_pattern.is_none() {
if let Some(source) = crate::blocker_interactions::integration::BlockerIssueSource::current() {
source.display_relative()
} else if args.blocker {
bail!("No blocker issue set. Use `todo blocker set <pattern>` first.")
} else {
String::new()
}
} else {
args.url_or_pattern.as_deref().unwrap_or("").trim().to_string()
};
let input = input.as_str();
if args.touch {
let source = parse_touch_path(input, args.parent, offline).await?;
let is_create = source.local_path.clone().resolve_parent(FsReader)?.search().is_err();
let issue = if is_create {
let index = *source.index();
let project_is_virtual = Local::is_virtual_project(index.repo_info());
Issue::pending_from_descriptor(&index, project_is_virtual)
} else {
Issue::load(source).await?
};
let project_is_virtual = issue.identity.is_virtual;
if is_create {
if !issue.identity.parent_index.index().is_empty() {
println!("Creating pending sub-issue: {}", issue.contents.title);
} else {
println!("Creating pending issue: {}", issue.contents.title);
}
if !project_is_virtual {
println!("Issue will be created on Github when you save and sync.");
}
} else {
println!("Found existing issue: {}", issue.contents.title);
}
modify_and_sync_issue(issue, offline || project_is_virtual, make_modifier(open_at_blocker), local_sync_opts()).await?;
return Ok(());
}
let (issue, sync_opts, effective_offline) = if args.last {
let cache_path = v_utils::xdg_cache_file!("last_modified_issue");
let index_str = std::fs::read_to_string(&cache_path).map_err(|_| eyre!("No last modified issue recorded. Open an issue first."))?;
let index: IssueIndex = index_str.parse()?;
let source = LocalIssueSource::<FsReader>::build(LocalPath::new(index)).await?;
let issue = Issue::load(source).await?;
(issue, local_sync_opts(), offline)
} else if github::is_github_issue_url(input) {
if offline {
bail!("Cannot fetch issue from URL in offline mode");
}
let (owner, repo, issue_number) = github::parse_github_issue_url(input)?;
let existing_path = Local::find_by_number(RepoInfo::new(&owner, &repo), issue_number, FsReader);
let issue = if existing_path.is_some() && args.reset {
println!("Resetting to remote state...");
let url = format!("https://github.com/{owner}/{repo}/issues/{issue_number}");
let link = IssueLink::parse(&url).expect("valid URL");
let source = RemoteSource::build(link, None)?;
let mut issue = Issue::load(source).await?;
<Issue as Sink<LocalFs>>::sink(&mut issue, None).await?;
<Issue as Sink<Consensus>>::sink(&mut issue, None).await?;
let conflict_path = tedi::local::conflict::conflict_file_path(&owner);
if conflict_path.exists() {
std::fs::remove_file(&conflict_path)?;
}
issue
} else if let Some(path) = existing_path {
println!("Found existing local file, will sync with remote...");
let source = LocalIssueSource::<FsReader>::build_from_path(&path).await?;
Issue::load(source).await?
} else {
println!("Fetching issue #{issue_number} from {owner}/{repo}...");
let url = format!("https://github.com/{owner}/{repo}/issues/{issue_number}");
let link = IssueLink::parse(&url).expect("valid URL");
let source = RemoteSource::build(link, None)?;
let mut issue = Issue::load(source).await?;
<Issue as Sink<LocalFs>>::sink(&mut issue, None).await?;
println!("Stored issue");
<Issue as Sink<Consensus>>::sink(&mut issue, None).await?;
issue
};
(issue, remote_sync_opts(), offline)
} else {
let input_path = Path::new(input);
let issue_file_path = if input_path.exists() && input_path.is_file() {
input_path.to_path_buf()
} else {
Local::fzf_issue(input, exact)?
};
let source = LocalIssueSource::<FsReader>::build_from_path(&issue_file_path).await?;
let issue = Issue::load(source).await?;
(issue, local_sync_opts(), offline)
};
modify_and_sync_issue(issue, effective_offline, make_modifier(open_at_blocker), sync_opts).await?;
Ok(())
}