cli: Add `rad patch diff` command
This commit is contained in:
parent
04bfbca15d
commit
91684cc9ca
|
|
@ -15,6 +15,7 @@ rad-patch - Manage radicle patches.
|
||||||
*rad patch* [<option>...] +
|
*rad patch* [<option>...] +
|
||||||
*rad patch* _list_ [--all|--merged|--open|--archived|--draft] [<option>...] +
|
*rad patch* _list_ [--all|--merged|--open|--archived|--draft] [<option>...] +
|
||||||
*rad patch* _show_ <patch-id> [<option>...] +
|
*rad patch* _show_ <patch-id> [<option>...] +
|
||||||
|
*rad patch* _diff_ <patch-id> [--revision <revision-id>] [<option>...] +
|
||||||
*rad patch* _archive_ <patch-id> [<option>...] +
|
*rad patch* _archive_ <patch-id> [<option>...] +
|
||||||
*rad patch* _update_ <patch-id> [<option>...] +
|
*rad patch* _update_ <patch-id> [<option>...] +
|
||||||
*rad patch* _checkout_ <patch-id> [<option>...] +
|
*rad patch* _checkout_ <patch-id> [<option>...] +
|
||||||
|
|
@ -52,6 +53,13 @@ Shows information on the given patch.
|
||||||
*--patch*, *-p*:: Show the patch changes in git patch format
|
*--patch*, *-p*:: Show the patch changes in git patch format
|
||||||
*--verbose*, *-v*:: Show additional information about the patch
|
*--verbose*, *-v*:: Show additional information about the patch
|
||||||
|
|
||||||
|
=== diff
|
||||||
|
|
||||||
|
Outputs the patch diff, using Radicle's diffing tool.
|
||||||
|
|
||||||
|
*<patch-id>*:: The patch to diff
|
||||||
|
*--revision*, *-r <revision-id>*:: The revision to diff (default: latest)
|
||||||
|
|
||||||
=== edit
|
=== edit
|
||||||
|
|
||||||
Edits a patch revision comment. To edit the patch title or description, pass
|
Edits a patch revision comment. To edit the patch title or description, pass
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
Using `rad patch diff`, we can output the patch diff:
|
||||||
|
|
||||||
|
``` ./README.md
|
||||||
|
Hello World!
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ git checkout -b feature/1
|
||||||
|
$ git add README.md
|
||||||
|
$ git commit -m "Add README" -q
|
||||||
|
$ git push rad HEAD:refs/patches
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ rad patch diff e432b04
|
||||||
|
╭───────────────────────────╮
|
||||||
|
│ README.md +1 ❲created❳ │
|
||||||
|
├───────────────────────────┤
|
||||||
|
│ @@ -0,0 +1,1 @@ │
|
||||||
|
│ 1 + Hello World! │
|
||||||
|
╰───────────────────────────╯
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If we add another file and update the patch, we can see it in the diff.
|
||||||
|
|
||||||
|
``` ./RADICLE.md
|
||||||
|
Hello Radicle!
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ git add RADICLE.md
|
||||||
|
$ git commit --amend -q
|
||||||
|
$ git push -f
|
||||||
|
```
|
||||||
|
```
|
||||||
|
$ rad patch diff e432b04
|
||||||
|
╭─────────────────────────────╮
|
||||||
|
│ RADICLE.md +1 ❲created❳ │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ @@ -0,0 +1,1 @@ │
|
||||||
|
│ 1 + Hello Radicle! │
|
||||||
|
╰─────────────────────────────╯
|
||||||
|
|
||||||
|
╭─────────────────────────────╮
|
||||||
|
│ README.md +1 ❲created❳ │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ @@ -0,0 +1,1 @@ │
|
||||||
|
│ 1 + Hello World! │
|
||||||
|
╰─────────────────────────────╯
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Buf if we only want to see the changes from the first revision, we can do that
|
||||||
|
too.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rad patch diff e432b04 --revision e432b04
|
||||||
|
╭───────────────────────────╮
|
||||||
|
│ README.md +1 ❲created❳ │
|
||||||
|
├───────────────────────────┤
|
||||||
|
│ @@ -0,0 +1,1 @@ │
|
||||||
|
│ 1 + Hello World! │
|
||||||
|
╰───────────────────────────╯
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -10,6 +10,8 @@ mod comment;
|
||||||
mod common;
|
mod common;
|
||||||
#[path = "patch/delete.rs"]
|
#[path = "patch/delete.rs"]
|
||||||
mod delete;
|
mod delete;
|
||||||
|
#[path = "patch/diff.rs"]
|
||||||
|
mod diff;
|
||||||
#[path = "patch/edit.rs"]
|
#[path = "patch/edit.rs"]
|
||||||
mod edit;
|
mod edit;
|
||||||
#[path = "patch/label.rs"]
|
#[path = "patch/label.rs"]
|
||||||
|
|
@ -53,6 +55,7 @@ Usage
|
||||||
rad patch [<option>...]
|
rad patch [<option>...]
|
||||||
rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...]
|
rad patch list [--all|--merged|--open|--archived|--draft|--authored] [--author <did>]... [<option>...]
|
||||||
rad patch show <patch-id> [<option>...]
|
rad patch show <patch-id> [<option>...]
|
||||||
|
rad patch diff <patch-id> [<option>...]
|
||||||
rad patch archive <patch-id> [<option>...]
|
rad patch archive <patch-id> [<option>...]
|
||||||
rad patch update <patch-id> [<option>...]
|
rad patch update <patch-id> [<option>...]
|
||||||
rad patch checkout <patch-id> [<option>...]
|
rad patch checkout <patch-id> [<option>...]
|
||||||
|
|
@ -71,6 +74,10 @@ Show options
|
||||||
-p, --patch Show the actual patch diff
|
-p, --patch Show the actual patch diff
|
||||||
-v, --verbose Show additional information about the patch
|
-v, --verbose Show additional information about the patch
|
||||||
|
|
||||||
|
Diff options
|
||||||
|
|
||||||
|
-r, --revision <id> The revision to diff (default: latest)
|
||||||
|
|
||||||
Comment options
|
Comment options
|
||||||
|
|
||||||
-m, --message <string> Provide a comment message via the command-line
|
-m, --message <string> Provide a comment message via the command-line
|
||||||
|
|
@ -81,14 +88,13 @@ Edit options
|
||||||
|
|
||||||
Review options
|
Review options
|
||||||
|
|
||||||
--revision <id> Review the given revision of the patch
|
-r, --revision <id> Review the given revision of the patch
|
||||||
-p, --patch Review by patch hunks
|
-p, --patch Review by patch hunks
|
||||||
--hunk <index> Only review a specific hunk
|
--hunk <index> Only review a specific hunk
|
||||||
--accept Accept a patch or set of hunks
|
--accept Accept a patch or set of hunks
|
||||||
--reject Reject a patch or set of hunks
|
--reject Reject a patch or set of hunks
|
||||||
-U, --unified <n> Generate diffs with <n> lines of context instead of the usual three
|
-U, --unified <n> Generate diffs with <n> lines of context instead of the usual three
|
||||||
-d, --delete Delete a review draft
|
-d, --delete Delete a review draft
|
||||||
-r, --revision Review a patch revision
|
|
||||||
-m, --message [<string>] Provide a comment with the review (default: prompt)
|
-m, --message [<string>] Provide a comment with the review (default: prompt)
|
||||||
|
|
||||||
Assign options
|
Assign options
|
||||||
|
|
@ -146,6 +152,7 @@ Other options
|
||||||
pub enum OperationName {
|
pub enum OperationName {
|
||||||
Assign,
|
Assign,
|
||||||
Show,
|
Show,
|
||||||
|
Diff,
|
||||||
Update,
|
Update,
|
||||||
Archive,
|
Archive,
|
||||||
Delete,
|
Delete,
|
||||||
|
|
@ -200,6 +207,10 @@ pub enum Operation {
|
||||||
patch_id: Rev,
|
patch_id: Rev,
|
||||||
diff: bool,
|
diff: bool,
|
||||||
},
|
},
|
||||||
|
Diff {
|
||||||
|
patch_id: Rev,
|
||||||
|
revision_id: Option<Rev>,
|
||||||
|
},
|
||||||
Update {
|
Update {
|
||||||
patch_id: Rev,
|
patch_id: Rev,
|
||||||
base_id: Option<Rev>,
|
base_id: Option<Rev>,
|
||||||
|
|
@ -267,7 +278,10 @@ impl Operation {
|
||||||
| Operation::Edit { .. }
|
| Operation::Edit { .. }
|
||||||
| Operation::Redact { .. }
|
| Operation::Redact { .. }
|
||||||
| Operation::Set { .. } => true,
|
| Operation::Set { .. } => true,
|
||||||
Operation::Show { .. } | Operation::Checkout { .. } | Operation::List { .. } => false,
|
Operation::Show { .. }
|
||||||
|
| Operation::Diff { .. }
|
||||||
|
| Operation::Checkout { .. }
|
||||||
|
| Operation::List { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,8 +374,10 @@ impl Args for Options {
|
||||||
reply_to = Some(rev);
|
reply_to = Some(rev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Review options.
|
// Review/diff options.
|
||||||
Long("revision") if op == Some(OperationName::Review) => {
|
Long("revision") | Short('r')
|
||||||
|
if op == Some(OperationName::Review) || op == Some(OperationName::Diff) =>
|
||||||
|
{
|
||||||
let val = parser.value()?;
|
let val = parser.value()?;
|
||||||
let rev = term::args::rev(&val)?;
|
let rev = term::args::rev(&val)?;
|
||||||
|
|
||||||
|
|
@ -511,6 +527,7 @@ impl Args for Options {
|
||||||
"y" | "ready" => op = Some(OperationName::Ready),
|
"y" | "ready" => op = Some(OperationName::Ready),
|
||||||
"e" | "edit" => op = Some(OperationName::Edit),
|
"e" | "edit" => op = Some(OperationName::Edit),
|
||||||
"r" | "redact" => op = Some(OperationName::Redact),
|
"r" | "redact" => op = Some(OperationName::Redact),
|
||||||
|
"diff" => op = Some(OperationName::Diff),
|
||||||
"assign" => op = Some(OperationName::Assign),
|
"assign" => op = Some(OperationName::Assign),
|
||||||
"label" => op = Some(OperationName::Label),
|
"label" => op = Some(OperationName::Label),
|
||||||
"comment" => op = Some(OperationName::Comment),
|
"comment" => op = Some(OperationName::Comment),
|
||||||
|
|
@ -526,6 +543,7 @@ impl Args for Options {
|
||||||
if patch_id.is_none()
|
if patch_id.is_none()
|
||||||
&& [
|
&& [
|
||||||
Some(OperationName::Show),
|
Some(OperationName::Show),
|
||||||
|
Some(OperationName::Diff),
|
||||||
Some(OperationName::Update),
|
Some(OperationName::Update),
|
||||||
Some(OperationName::Delete),
|
Some(OperationName::Delete),
|
||||||
Some(OperationName::Archive),
|
Some(OperationName::Archive),
|
||||||
|
|
@ -553,6 +571,10 @@ impl Args for Options {
|
||||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||||
diff,
|
diff,
|
||||||
},
|
},
|
||||||
|
OperationName::Diff => Operation::Diff {
|
||||||
|
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||||
|
revision_id,
|
||||||
|
},
|
||||||
OperationName::Delete => Operation::Delete {
|
OperationName::Delete => Operation::Delete {
|
||||||
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?,
|
||||||
},
|
},
|
||||||
|
|
@ -652,6 +674,17 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
&workdir,
|
&workdir,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
Operation::Diff {
|
||||||
|
patch_id,
|
||||||
|
revision_id,
|
||||||
|
} => {
|
||||||
|
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||||
|
let revision_id = revision_id
|
||||||
|
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
|
||||||
|
.transpose()?
|
||||||
|
.map(patch::RevisionId::from);
|
||||||
|
diff::run(&patch_id, revision_id, &repository)?;
|
||||||
|
}
|
||||||
Operation::Update {
|
Operation::Update {
|
||||||
ref patch_id,
|
ref patch_id,
|
||||||
ref base_id,
|
ref base_id,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use radicle::cob::patch;
|
||||||
|
use radicle::storage::git::Repository;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
patch_id: &PatchId,
|
||||||
|
revision_id: Option<patch::RevisionId>,
|
||||||
|
stored: &Repository,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let patches = patch::Patches::open(stored)?;
|
||||||
|
let Some(patch) = patches.get(patch_id)? else {
|
||||||
|
anyhow::bail!("Patch `{patch_id}` not found");
|
||||||
|
};
|
||||||
|
let revision = if let Some(r) = revision_id {
|
||||||
|
patch
|
||||||
|
.revision(&r)
|
||||||
|
.ok_or(anyhow!("revision `{r}` not found"))?
|
||||||
|
} else {
|
||||||
|
let (_, r) = patch.latest();
|
||||||
|
r
|
||||||
|
};
|
||||||
|
let (from, to) = revision.range();
|
||||||
|
|
||||||
|
process::Command::new("rad")
|
||||||
|
.current_dir(stored.path())
|
||||||
|
.args(["diff", from.to_string().as_str(), to.to_string().as_str()])
|
||||||
|
.stdout(process::Stdio::inherit())
|
||||||
|
.stderr(process::Stdio::inherit())
|
||||||
|
.spawn()?
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -100,7 +100,7 @@ pub fn row(
|
||||||
) -> anyhow::Result<[term::Line; 9]> {
|
) -> anyhow::Result<[term::Line; 9]> {
|
||||||
let state = patch.state();
|
let state = patch.state();
|
||||||
let (_, revision) = patch.latest();
|
let (_, revision) = patch.latest();
|
||||||
let (from, to) = patch.range(repository)?;
|
let (from, to) = revision.range();
|
||||||
let stats = common::diff_stats(repository.raw(), &from, &to)?;
|
let stats = common::diff_stats(repository.raw(), &from, &to)?;
|
||||||
let author = patch.author().id;
|
let author = patch.author().id;
|
||||||
let (alias, did) = Author::new(&author, profile).labels();
|
let (alias, did) = Author::new(&author, profile).labels();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::terminal as term;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<()> {
|
fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<()> {
|
||||||
let (from, to) = patch.range(stored)?;
|
let (from, to) = patch.range()?;
|
||||||
let range = format!("{}..{}", from, to);
|
let range = format!("{}..{}", from, to);
|
||||||
|
|
||||||
process::Command::new("git")
|
process::Command::new("git")
|
||||||
|
|
@ -28,7 +28,7 @@ fn show_patch_diff(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<
|
||||||
}
|
}
|
||||||
|
|
||||||
fn patch_commits(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<Vec<term::Line>> {
|
fn patch_commits(patch: &patch::Patch, stored: &Repository) -> anyhow::Result<Vec<term::Line>> {
|
||||||
let (from, to) = patch.range(stored)?;
|
let (from, to) = patch.range()?;
|
||||||
let range = format!("{}..{}", from, to);
|
let range = format!("{}..{}", from, to);
|
||||||
|
|
||||||
let mut revwalk = stored.revwalk(*patch.head())?;
|
let mut revwalk = stored.revwalk(*patch.head())?;
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ use crate::terminal::highlight::{Highlighter, Theme};
|
||||||
use super::unified_diff::{Decode, HunkHeader};
|
use super::unified_diff::{Decode, HunkHeader};
|
||||||
|
|
||||||
/// Blob returned by the [`Repo`] trait.
|
/// Blob returned by the [`Repo`] trait.
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum Blob {
|
pub enum Blob {
|
||||||
Binary,
|
Binary,
|
||||||
|
Empty,
|
||||||
Plain(Vec<u8>),
|
Plain(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,10 +34,16 @@ impl Repo for git::raw::Repository {
|
||||||
|
|
||||||
if blob.is_binary() {
|
if blob.is_binary() {
|
||||||
Ok(Blob::Binary)
|
Ok(Blob::Binary)
|
||||||
|
} else {
|
||||||
|
let content = blob.content();
|
||||||
|
|
||||||
|
if content.is_empty() {
|
||||||
|
Ok(Blob::Empty)
|
||||||
} else {
|
} else {
|
||||||
Ok(Blob::Plain(blob.content().to_vec()))
|
Ok(Blob::Plain(blob.content().to_vec()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn file(&self, path: &Path) -> Option<Blob> {
|
fn file(&self, path: &Path) -> Option<Blob> {
|
||||||
self.workdir()
|
self.workdir()
|
||||||
|
|
@ -226,6 +234,14 @@ impl ToPretty for DiffContent {
|
||||||
|
|
||||||
match context {
|
match context {
|
||||||
FileDiff::Moved(_) | FileDiff::Copied(_) => {}
|
FileDiff::Moved(_) | FileDiff::Copied(_) => {}
|
||||||
|
FileDiff::Added(_) if blobs.new.is_none() => {
|
||||||
|
vstack = vstack.divider();
|
||||||
|
vstack.push(term::Line::new(term::format::italic("Empty file")));
|
||||||
|
}
|
||||||
|
FileDiff::Deleted(_) if blobs.old.is_none() => {
|
||||||
|
vstack = vstack.divider();
|
||||||
|
vstack.push(term::Line::new(term::format::italic("Empty file")));
|
||||||
|
}
|
||||||
FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => {
|
FileDiff::Added(_) | FileDiff::Deleted(_) | FileDiff::Modified(_) => {
|
||||||
vstack = vstack.divider();
|
vstack = vstack.divider();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,20 @@ fn rad_patch() {
|
||||||
test("examples/rad-patch.md", working.path(), Some(home), []).unwrap();
|
test("examples/rad-patch.md", working.path(), Some(home), []).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_patch_diff() {
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let profile = environment.profile(config::profile("alice"));
|
||||||
|
let working = tempfile::tempdir().unwrap();
|
||||||
|
let home = &profile.home;
|
||||||
|
|
||||||
|
// Setup a test repository.
|
||||||
|
fixtures::repository(working.path());
|
||||||
|
|
||||||
|
test("examples/rad-init.md", working.path(), Some(home), []).unwrap();
|
||||||
|
test("examples/rad-patch-diff.md", working.path(), Some(home), []).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rad_patch_checkout() {
|
fn rad_patch_checkout() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ pub enum Error {
|
||||||
#[error("identity doc failed to load: {0}")]
|
#[error("identity doc failed to load: {0}")]
|
||||||
Doc(#[from] DocError),
|
Doc(#[from] DocError),
|
||||||
/// Identity document is missing.
|
/// Identity document is missing.
|
||||||
#[error("missing identity docuemnt")]
|
#[error("missing identity document")]
|
||||||
MissingIdentity,
|
MissingIdentity,
|
||||||
/// Error loading the document payload.
|
/// Error loading the document payload.
|
||||||
#[error("payload failed to load: {0}")]
|
#[error("payload failed to load: {0}")]
|
||||||
|
|
@ -118,6 +118,9 @@ pub enum Error {
|
||||||
/// An illegal action.
|
/// An illegal action.
|
||||||
#[error("action is not allowed: {0}")]
|
#[error("action is not allowed: {0}")]
|
||||||
NotAllowed(EntryId),
|
NotAllowed(EntryId),
|
||||||
|
/// Revision not found.
|
||||||
|
#[error("revision not found: {0}")]
|
||||||
|
RevisionNotFound(RevisionId),
|
||||||
/// Initialization failed.
|
/// Initialization failed.
|
||||||
#[error("initialization failed: {0}")]
|
#[error("initialization failed: {0}")]
|
||||||
Init(&'static str),
|
Init(&'static str),
|
||||||
|
|
@ -521,15 +524,8 @@ impl Patch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the commit range of this patch.
|
/// Get the commit range of this patch.
|
||||||
pub fn range<R: ReadRepository>(
|
pub fn range(&self) -> Result<(git::Oid, git::Oid), git::ext::Error> {
|
||||||
&self,
|
return Ok((*self.base(), *self.head()));
|
||||||
repo: &R,
|
|
||||||
) -> Result<(git::Oid, git::Oid), git::ext::Error> {
|
|
||||||
if self.is_merged() {
|
|
||||||
Ok((*self.base(), *self.head()))
|
|
||||||
} else {
|
|
||||||
Ok((self.merge_base(repo)?, *self.head()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index of latest revision in the revisions list.
|
/// Index of latest revision in the revisions list.
|
||||||
|
|
@ -1331,6 +1327,11 @@ impl Revision {
|
||||||
self.oid
|
self.oid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the commit range of this revision.
|
||||||
|
pub fn range(&self) -> (git::Oid, git::Oid) {
|
||||||
|
(self.base, self.oid)
|
||||||
|
}
|
||||||
|
|
||||||
/// When this revision was created.
|
/// When this revision was created.
|
||||||
pub fn timestamp(&self) -> Timestamp {
|
pub fn timestamp(&self) -> Timestamp {
|
||||||
self.timestamp
|
self.timestamp
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue