Fix repo list, starts repo create

Changed files
+379 -103
crates
+1
Cargo.lock
···
"anyhow",
"atrium-api",
"atrium-xrpc-client",
+
"chrono",
"reqwest",
"serde",
"serde_json",
+1 -1
crates/tangled-api/Cargo.toml
···
serde_json = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true, features = ["full"] }
+
chrono = { workspace = true }
# Optionally depend on ATrium (wired later as endpoints solidify)
atrium-api = { workspace = true, optional = true }
···
[features]
default = []
atrium = ["dep:atrium-api", "dep:atrium-xrpc-client"]
-
+184 -32
crates/tangled-api/src/client.rs
···
base_url: String,
}
+
const REPO_CREATE: &str = "sh.tangled.repo.create";
+
+
impl Default for TangledClient {
+
fn default() -> Self {
+
Self::new("https://tngl.sh")
+
}
+
}
+
impl TangledClient {
pub fn new(base_url: impl Into<String>) -> Self {
-
Self { base_url: base_url.into() }
-
}
-
-
pub fn default() -> Self {
-
Self::new("https://tangled.org")
+
Self {
+
base_url: base_url.into(),
+
}
}
fn xrpc_url(&self, method: &str) -> String {
-
format!(
-
"{}/xrpc/{}",
-
self.base_url.trim_end_matches('/'),
-
method
-
)
+
format!("{}/xrpc/{}", self.base_url.trim_end_matches('/'), method)
}
async fn post_json<TReq: Serialize, TRes: DeserializeOwned>(
···
.post(url)
.header(reqwest::header::CONTENT_TYPE, "application/json");
if let Some(token) = bearer {
-
reqb = reqb.header(
-
reqwest::header::AUTHORIZATION,
-
format!("Bearer {}", token),
-
);
+
reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token));
}
let res = reqb.json(req).send().await?;
let status = res.status();
···
) -> Result<TRes> {
let url = self.xrpc_url(method);
let client = reqwest::Client::new();
-
let mut reqb = client.get(url).query(&params);
+
let mut reqb = client
+
.get(&url)
+
.query(&params)
+
.header(reqwest::header::ACCEPT, "application/json");
if let Some(token) = bearer {
-
reqb = reqb.header(
-
reqwest::header::AUTHORIZATION,
-
format!("Bearer {}", token),
-
);
+
reqb = reqb.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token));
}
let res = reqb.send().await?;
let status = res.status();
+
let body = res.text().await.unwrap_or_default();
if !status.is_success() {
-
let body = res.text().await.unwrap_or_default();
-
return Err(anyhow!("{}: {}", status, body));
+
return Err(anyhow!("GET {} -> {}: {}", url, status, body));
}
-
Ok(res.json::<TRes>().await?)
+
serde_json::from_str::<TRes>(&body).map_err(|e| {
+
let snippet = body.chars().take(300).collect::<String>();
+
anyhow!(
+
"error decoding response from {}: {}\nBody (first 300 chars): {}",
+
url,
+
e,
+
snippet
+
)
+
})
}
pub async fn login_with_password(
···
starred: bool,
bearer: Option<&str>,
) -> Result<Vec<Repository>> {
+
// NOTE: Repo listing is done via the user's PDS using com.atproto.repo.listRecords
+
// for the collection "sh.tangled.repo". This does not go through the Tangled API base.
+
// Here, `self.base_url` must be the PDS base (e.g., https://bsky.social).
+
// Resolve handle to DID if needed
+
let did = match user {
+
Some(u) if u.starts_with("did:") => u.to_string(),
+
Some(handle) => {
+
#[derive(Deserialize)]
+
struct Res {
+
did: String,
+
}
+
let params = [("handle", handle.to_string())];
+
let res: Res = self
+
.get_json("com.atproto.identity.resolveHandle", &params, bearer)
+
.await?;
+
res.did
+
}
+
None => {
+
return Err(anyhow!(
+
"missing user for list_repos; provide handle or DID"
+
));
+
}
+
};
+
#[derive(Deserialize)]
-
struct Envelope {
-
repos: Vec<Repository>,
+
struct RecordItem {
+
value: Repository,
}
-
let mut q = vec![];
-
if let Some(u) = user {
-
q.push(("user", u.to_string()));
+
#[derive(Deserialize)]
+
struct ListRes {
+
#[serde(default)]
+
records: Vec<RecordItem>,
}
+
+
let params = vec![
+
("repo", did),
+
("collection", "sh.tangled.repo".to_string()),
+
("limit", "100".to_string()),
+
];
+
+
let res: ListRes = self
+
.get_json("com.atproto.repo.listRecords", &params, bearer)
+
.await?;
+
let mut repos: Vec<Repository> = res.records.into_iter().map(|r| r.value).collect();
+
// Apply optional filters client-side
if let Some(k) = knot {
-
q.push(("knot", k.to_string()));
+
repos.retain(|r| r.knot.as_deref().unwrap_or("") == k);
}
if starred {
-
q.push(("starred", true.to_string()));
+
// TODO: implement starred filtering when API is available. For now, no-op.
}
-
let env: Envelope = self
-
.get_json("sh.tangled.repo.list", &q, bearer)
+
Ok(repos)
+
}
+
+
pub async fn create_repo(&self, opts: CreateRepoOptions<'_>) -> Result<()> {
+
// 1) Create the sh.tangled.repo record on the user's PDS
+
#[derive(Serialize)]
+
struct Record<'a> {
+
name: &'a str,
+
knot: &'a str,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
description: Option<&'a str>,
+
#[serde(rename = "createdAt")]
+
created_at: String,
+
}
+
#[derive(Serialize)]
+
struct CreateRecordReq<'a> {
+
repo: &'a str,
+
collection: &'a str,
+
validate: bool,
+
record: Record<'a>,
+
}
+
#[derive(Deserialize)]
+
struct CreateRecordRes {
+
uri: String,
+
}
+
+
let now = chrono::Utc::now().to_rfc3339();
+
let rec = Record {
+
name: opts.name,
+
knot: opts.knot,
+
description: opts.description,
+
created_at: now,
+
};
+
let create_req = CreateRecordReq {
+
repo: opts.did,
+
collection: "sh.tangled.repo",
+
validate: true,
+
record: rec,
+
};
+
+
let pds_client = TangledClient::new(opts.pds_base);
+
let created: CreateRecordRes = pds_client
+
.post_json(
+
"com.atproto.repo.createRecord",
+
&create_req,
+
Some(opts.access_jwt),
+
)
.await?;
-
Ok(env.repos)
+
+
// Extract rkey from at-uri: at://did/collection/rkey
+
let rkey = created
+
.uri
+
.rsplit('/')
+
.next()
+
.ok_or_else(|| anyhow!("failed to parse rkey from uri"))?;
+
+
// 2) Obtain a service auth token for the Tangled server (aud = did:web:<host>)
+
let host = self
+
.base_url
+
.trim_end_matches('/')
+
.strip_prefix("https://")
+
.or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://"))
+
.ok_or_else(|| anyhow!("invalid base_url"))?;
+
let audience = format!("did:web:{}", host);
+
+
#[derive(Deserialize)]
+
struct GetSARes {
+
token: String,
+
}
+
let params = [
+
("aud", audience),
+
("exp", (chrono::Utc::now().timestamp() + 600).to_string()),
+
];
+
let sa: GetSARes = pds_client
+
.get_json(
+
"com.atproto.server.getServiceAuth",
+
&params,
+
Some(opts.access_jwt),
+
)
+
.await?;
+
+
// 3) Call sh.tangled.repo.create with the rkey
+
#[derive(Serialize)]
+
struct CreateRepoReq<'a> {
+
rkey: &'a str,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
#[serde(rename = "defaultBranch")]
+
default_branch: Option<&'a str>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
source: Option<&'a str>,
+
}
+
let req = CreateRepoReq {
+
rkey,
+
default_branch: opts.default_branch,
+
source: opts.source,
+
};
+
// No output expected on success
+
let _: serde_json::Value = self.post_json(REPO_CREATE, &req, Some(&sa.token)).await?;
+
Ok(())
}
}
···
pub name: String,
pub knot: Option<String>,
pub description: Option<String>,
+
#[serde(default)]
pub private: bool,
}
+
+
#[derive(Debug, Clone)]
+
pub struct CreateRepoOptions<'a> {
+
pub did: &'a str,
+
pub name: &'a str,
+
pub knot: &'a str,
+
pub description: Option<&'a str>,
+
pub default_branch: Option<&'a str>,
+
pub source: Option<&'a str>,
+
pub pds_base: &'a str,
+
pub access_jwt: &'a str,
+
}
-1
crates/tangled-api/src/lib.rs
···
pub mod client;
pub use client::TangledClient;
-
+3
crates/tangled-cli/src/cli.rs
···
pub user: Option<String>,
#[arg(long, default_value_t = false)]
pub starred: bool,
+
/// Tangled API base URL (overrides env)
+
#[arg(long)]
+
pub base: Option<String>,
}
#[derive(Args, Debug, Clone)]
+5 -4
crates/tangled-cli/src/commands/auth.rs
···
Some(p) => p,
None => Password::new().with_prompt("Password").interact()?,
};
-
let pds = args.pds.unwrap_or_else(|| "https://bsky.social".to_string());
+
let pds = args
+
.pds
+
.unwrap_or_else(|| "https://bsky.social".to_string());
let client = tangled_api::TangledClient::new(&pds);
-
let session = client
-
.login_with_password(&handle, &password, &pds)
-
.await?;
+
let mut session = client.login_with_password(&handle, &password, &pds).await?;
+
session.pds = Some(pds.clone());
SessionManager::default().save(&session)?;
println!("Logged in as '{}' ({})", session.handle, session.did);
Ok(())
+24 -10
crates/tangled-cli/src/commands/issue.rs
···
+
use crate::cli::{
+
Cli, IssueCommand, IssueCommentArgs, IssueCreateArgs, IssueEditArgs, IssueListArgs,
+
IssueShowArgs,
+
};
use anyhow::Result;
-
use crate::cli::{Cli, IssueCommand, IssueListArgs, IssueCreateArgs, IssueShowArgs, IssueEditArgs, IssueCommentArgs};
pub async fn run(_cli: &Cli, cmd: IssueCommand) -> Result<()> {
match cmd {
···
}
async fn list(args: IssueListArgs) -> Result<()> {
-
println!("Issue list (stub) repo={:?} state={:?} author={:?} label={:?} assigned={:?}",
-
args.repo, args.state, args.author, args.label, args.assigned);
+
println!(
+
"Issue list (stub) repo={:?} state={:?} author={:?} label={:?} assigned={:?}",
+
args.repo, args.state, args.author, args.label, args.assigned
+
);
Ok(())
}
async fn create(args: IssueCreateArgs) -> Result<()> {
-
println!("Issue create (stub) repo={:?} title={:?} body={:?} labels={:?} assign={:?}",
-
args.repo, args.title, args.body, args.label, args.assign);
+
println!(
+
"Issue create (stub) repo={:?} title={:?} body={:?} labels={:?} assign={:?}",
+
args.repo, args.title, args.body, args.label, args.assign
+
);
Ok(())
}
async fn show(args: IssueShowArgs) -> Result<()> {
-
println!("Issue show (stub) id={} comments={} json={}", args.id, args.comments, args.json);
+
println!(
+
"Issue show (stub) id={} comments={} json={}",
+
args.id, args.comments, args.json
+
);
Ok(())
}
async fn edit(args: IssueEditArgs) -> Result<()> {
-
println!("Issue edit (stub) id={} title={:?} body={:?} state={:?}",
-
args.id, args.title, args.body, args.state);
+
println!(
+
"Issue edit (stub) id={} title={:?} body={:?} state={:?}",
+
args.id, args.title, args.body, args.state
+
);
Ok(())
}
async fn comment(args: IssueCommentArgs) -> Result<()> {
-
println!("Issue comment (stub) id={} close={} body={:?}", args.id, args.close, args.body);
+
println!(
+
"Issue comment (stub) id={} close={} body={:?}",
+
args.id, args.close, args.body
+
);
Ok(())
}
-
+9 -4
crates/tangled-cli/src/commands/knot.rs
···
+
use crate::cli::{Cli, KnotAddArgs, KnotCommand, KnotListArgs, KnotRefArgs, KnotVerifyArgs};
use anyhow::Result;
-
use crate::cli::{Cli, KnotCommand, KnotListArgs, KnotAddArgs, KnotVerifyArgs, KnotRefArgs};
pub async fn run(_cli: &Cli, cmd: KnotCommand) -> Result<()> {
match cmd {
···
}
async fn list(args: KnotListArgs) -> Result<()> {
-
println!("Knot list (stub) public={} owned={}", args.public, args.owned);
+
println!(
+
"Knot list (stub) public={} owned={}",
+
args.public, args.owned
+
);
Ok(())
}
async fn add(args: KnotAddArgs) -> Result<()> {
-
println!("Knot add (stub) url={} did={:?} name={:?} verify={}", args.url, args.did, args.name, args.verify);
+
println!(
+
"Knot add (stub) url={} did={:?} name={:?} verify={}",
+
args.url, args.did, args.name, args.verify
+
);
Ok(())
}
···
println!("Knot remove (stub) url={}", args.url);
Ok(())
}
-
+3 -7
crates/tangled-cli/src/commands/mod.rs
···
pub mod auth;
-
pub mod repo;
pub mod issue;
+
pub mod knot;
pub mod pr;
-
pub mod knot;
+
pub mod repo;
pub mod spindle;
use anyhow::Result;
-
use colored::Colorize;
use crate::cli::{Cli, Command};
···
}
}
-
fn not_implemented(feature: &str) -> Result<()> {
-
eprintln!("{} {}", "[todo]".yellow().bold(), feature);
-
Ok(())
-
}
+
// All subcommands are currently implemented with stubs where needed.
+21 -11
crates/tangled-cli/src/commands/pr.rs
···
+
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrMergeArgs, PrReviewArgs, PrShowArgs};
use anyhow::Result;
-
use crate::cli::{Cli, PrCommand, PrCreateArgs, PrListArgs, PrShowArgs, PrReviewArgs, PrMergeArgs};
pub async fn run(_cli: &Cli, cmd: PrCommand) -> Result<()> {
match cmd {
···
}
async fn list(args: PrListArgs) -> Result<()> {
-
println!("PR list (stub) repo={:?} state={:?} author={:?} reviewer={:?}",
-
args.repo, args.state, args.author, args.reviewer);
+
println!(
+
"PR list (stub) repo={:?} state={:?} author={:?} reviewer={:?}",
+
args.repo, args.state, args.author, args.reviewer
+
);
Ok(())
}
async fn create(args: PrCreateArgs) -> Result<()> {
-
println!("PR create (stub) repo={:?} base={:?} head={:?} title={:?} draft={}",
-
args.repo, args.base, args.head, args.title, args.draft);
+
println!(
+
"PR create (stub) repo={:?} base={:?} head={:?} title={:?} draft={}",
+
args.repo, args.base, args.head, args.title, args.draft
+
);
Ok(())
}
async fn show(args: PrShowArgs) -> Result<()> {
-
println!("PR show (stub) id={} diff={} comments={} checks={}", args.id, args.diff, args.comments, args.checks);
+
println!(
+
"PR show (stub) id={} diff={} comments={} checks={}",
+
args.id, args.diff, args.comments, args.checks
+
);
Ok(())
}
async fn review(args: PrReviewArgs) -> Result<()> {
-
println!("PR review (stub) id={} approve={} request_changes={} comment={:?}",
-
args.id, args.approve, args.request_changes, args.comment);
+
println!(
+
"PR review (stub) id={} approve={} request_changes={} comment={:?}",
+
args.id, args.approve, args.request_changes, args.comment
+
);
Ok(())
}
async fn merge(args: PrMergeArgs) -> Result<()> {
-
println!("PR merge (stub) id={} squash={} rebase={} no_ff={}",
-
args.id, args.squash, args.rebase, args.no_ff);
+
println!(
+
"PR merge (stub) id={} squash={} rebase={} no_ff={}",
+
args.id, args.squash, args.rebase, args.no_ff
+
);
Ok(())
}
-
+82 -13
crates/tangled-cli/src/commands/repo.rs
···
-
use anyhow::Result;
-
use crate::cli::{Cli, RepoCommand, RepoCreateArgs, RepoInfoArgs, RepoListArgs, RepoCloneArgs, RepoDeleteArgs, RepoRefArgs};
+
use anyhow::{anyhow, Result};
+
use serde_json;
+
use tangled_config::session::SessionManager;
-
pub async fn run(_cli: &Cli, cmd: RepoCommand) -> Result<()> {
+
use crate::cli::{
+
Cli, OutputFormat, RepoCloneArgs, RepoCommand, RepoCreateArgs, RepoDeleteArgs, RepoInfoArgs,
+
RepoListArgs, RepoRefArgs,
+
};
+
+
pub async fn run(cli: &Cli, cmd: RepoCommand) -> Result<()> {
match cmd {
-
RepoCommand::List(args) => list(args).await,
+
RepoCommand::List(args) => list(cli, args).await,
RepoCommand::Create(args) => create(args).await,
RepoCommand::Clone(args) => clone(args).await,
RepoCommand::Info(args) => info(args).await,
···
}
}
-
async fn list(args: RepoListArgs) -> Result<()> {
-
println!("Listing repositories (stub) knot={:?} user={:?} starred={}",
-
args.knot, args.user, args.starred);
+
async fn list(cli: &Cli, args: RepoListArgs) -> Result<()> {
+
let mgr = SessionManager::default();
+
let session = match mgr.load()? {
+
Some(s) => s,
+
None => return Err(anyhow!("Please login first: tangled auth login")),
+
};
+
+
// Use the PDS to list repo records for the user
+
let pds = session
+
.pds
+
.clone()
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
+
.unwrap_or_else(|| "https://bsky.social".into());
+
let pds_client = tangled_api::TangledClient::new(pds);
+
// Default to the logged-in user handle if --user is not provided
+
let effective_user = args.user.as_deref().unwrap_or(session.handle.as_str());
+
let repos = pds_client
+
.list_repos(
+
Some(effective_user),
+
args.knot.as_deref(),
+
args.starred,
+
Some(session.access_jwt.as_str()),
+
)
+
.await?;
+
+
match cli.format {
+
OutputFormat::Json => {
+
let json = serde_json::to_string_pretty(&repos)?;
+
println!("{}", json);
+
}
+
OutputFormat::Table => {
+
println!("NAME\tKNOT\tPRIVATE");
+
for r in repos {
+
println!("{}\t{}\t{}", r.name, r.knot.unwrap_or_default(), r.private);
+
}
+
}
+
}
+
Ok(())
}
async fn create(args: RepoCreateArgs) -> Result<()> {
-
println!(
-
"Creating repo '{}' (stub) knot={:?} private={} init={} desc={:?}",
-
args.name, args.knot, args.private, args.init, args.description
-
);
+
let mgr = SessionManager::default();
+
let session = match mgr.load()? {
+
Some(s) => s,
+
None => return Err(anyhow!("Please login first: tangled auth login")),
+
};
+
+
let base = std::env::var("TANGLED_API_BASE").unwrap_or_else(|_| "https://tngl.sh".into());
+
let client = tangled_api::TangledClient::new(base);
+
+
// Determine PDS base and target knot hostname
+
let pds = session
+
.pds
+
.clone()
+
.or_else(|| std::env::var("TANGLED_PDS_BASE").ok())
+
.unwrap_or_else(|| "https://bsky.social".into());
+
let knot = args.knot.unwrap_or_else(|| "tngl.sh".to_string());
+
+
let opts = tangled_api::client::CreateRepoOptions {
+
did: &session.did,
+
name: &args.name,
+
knot: &knot,
+
description: args.description.as_deref(),
+
default_branch: None,
+
source: None,
+
pds_base: &pds,
+
access_jwt: &session.access_jwt,
+
};
+
client.create_repo(opts).await?;
+
+
println!("Created repo '{}' (knot: {})", args.name, knot);
Ok(())
}
async fn clone(args: RepoCloneArgs) -> Result<()> {
-
println!("Cloning repo '{}' (stub) https={} depth={:?}", args.repo, args.https, args.depth);
+
println!(
+
"Cloning repo '{}' (stub) https={} depth={:?}",
+
args.repo, args.https, args.depth
+
);
Ok(())
}
···
println!("Unstarring repo '{}' (stub)", args.repo);
Ok(())
}
-
+11 -4
crates/tangled-cli/src/commands/spindle.rs
···
+
use crate::cli::{
+
Cli, SpindleCommand, SpindleConfigArgs, SpindleListArgs, SpindleLogsArgs, SpindleRunArgs,
+
};
use anyhow::Result;
-
use crate::cli::{Cli, SpindleCommand, SpindleListArgs, SpindleConfigArgs, SpindleRunArgs, SpindleLogsArgs};
pub async fn run(_cli: &Cli, cmd: SpindleCommand) -> Result<()> {
match cmd {
···
}
async fn run_pipeline(args: SpindleRunArgs) -> Result<()> {
-
println!("Spindle run (stub) repo={:?} branch={:?} wait={}", args.repo, args.branch, args.wait);
+
println!(
+
"Spindle run (stub) repo={:?} branch={:?} wait={}",
+
args.repo, args.branch, args.wait
+
);
Ok(())
}
async fn logs(args: SpindleLogsArgs) -> Result<()> {
-
println!("Spindle logs (stub) job_id={} follow={} lines={:?}", args.job_id, args.follow, args.lines);
+
println!(
+
"Spindle logs (stub) job_id={} follow={} lines={:?}",
+
args.job_id, args.follow, args.lines
+
);
Ok(())
}
-
+1 -1
crates/tangled-cli/src/main.rs
···
mod commands;
use anyhow::Result;
-
use cli::Cli;
use clap::Parser;
+
use cli::Cli;
#[tokio::main]
async fn main() -> Result<()> {
+7 -4
crates/tangled-config/src/config.rs
···
pub knot: Option<String>,
pub editor: Option<String>,
pub pager: Option<String>,
-
#[serde(default = "default_format")]
+
#[serde(default = "default_format")]
pub format: String,
}
-
fn default_format() -> String { "table".to_string() }
+
fn default_format() -> String {
+
"table".to_string()
+
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthSection {
···
let path = path
.map(|p| p.to_path_buf())
.unwrap_or(default_config_path()?);
-
if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; }
+
if let Some(parent) = path.parent() {
+
std::fs::create_dir_all(parent)?;
+
}
let toml = toml::to_string_pretty(cfg)?;
fs::write(&path, toml)
.with_context(|| format!("Failed writing config file: {}", path.display()))?;
Ok(())
}
-
+13 -4
crates/tangled-config/src/keychain.rs
···
impl Keychain {
pub fn new(service: &str, account: &str) -> Self {
-
Self { service: service.into(), account: account.into() }
+
Self {
+
service: service.into(),
+
account: account.into(),
+
}
}
fn entry(&self) -> Result<Entry> {
···
}
pub fn set_password(&self, secret: &str) -> Result<()> {
-
self.entry()?.set_password(secret).map_err(|e| anyhow!("keyring error: {e}"))
+
self.entry()?
+
.set_password(secret)
+
.map_err(|e| anyhow!("keyring error: {e}"))
}
pub fn get_password(&self) -> Result<String> {
-
self.entry()?.get_password().map_err(|e| anyhow!("keyring error: {e}"))
+
self.entry()?
+
.get_password()
+
.map_err(|e| anyhow!("keyring error: {e}"))
}
pub fn delete_password(&self) -> Result<()> {
-
self.entry()?.delete_credential().map_err(|e| anyhow!("keyring error: {e}"))
+
self.entry()?
+
.delete_credential()
+
.map_err(|e| anyhow!("keyring error: {e}"))
}
}
+1 -2
crates/tangled-config/src/lib.rs
···
pub mod config;
-
pub mod session;
pub mod keychain;
-
+
pub mod session;
+13 -3
crates/tangled-config/src/session.rs
···
pub did: String,
pub handle: String,
#[serde(default)]
+
pub pds: Option<String>,
+
#[serde(default)]
pub created_at: DateTime<Utc>,
}
···
refresh_jwt: String::new(),
did: String::new(),
handle: String::new(),
+
pds: None,
created_at: Utc::now(),
}
}
···
impl Default for SessionManager {
fn default() -> Self {
-
Self { service: "tangled-cli".into(), account: "default".into() }
+
Self {
+
service: "tangled-cli".into(),
+
account: "default".into(),
+
}
}
}
impl SessionManager {
-
pub fn new(service: &str, account: &str) -> Self { Self { service: service.into(), account: account.into() } }
+
pub fn new(service: &str, account: &str) -> Self {
+
Self {
+
service: service.into(),
+
account: account.into(),
+
}
+
}
pub fn save(&self, session: &Session) -> Result<()> {
let keychain = Keychain::new(&self.service, &self.account);
···
keychain.delete_password()
}
}
-
-1
crates/tangled-git/src/lib.rs
···
pub mod operations;
-
-1
crates/tangled-git/src/operations.rs
···
// TODO: support ssh/https and depth
bail!("clone_repo not implemented")
}
-