mirror of
https://github.com/nushell/nushell.git
synced 2024-12-04 22:33:50 +01:00
1f47d72e86
# Description Adds a simple command for importing history between different file formats. It essentially opens the history of the format opposite of the one currently selected, and writes new items to the current history. It also supports piping, because why not. As more history backends are added, this may need to be extended - either make the source explicit, or autodetect based on existing files. For now it should be good though. This should replace some of the work-arounds mentioned in https://github.com/nushell/nushell/issues/9403. I suspect it will have at least one problem: https://github.com/nushell/nushell/issues/9403 mentions the history file might be locked on Windows. That being said, I was able to successfully import plaintext history into sqlite on Linux, so the command should be functional at least in that environment. The locking issue could be solved later by plumbing reedline history to the command (so that it doesn't have to reopen it). But first, I want to get some general input on the approach. # User-Facing Changes New command: `history import` # Tests + Formatting There's a unit test, but didn't add a proper integration test yet. Not entirely sure how - I see there's the `nu!` macro for that, but not sure how feasible it's to inspect history generated by commands ran that way. Could use a hint.2
187 lines
5.0 KiB
Rust
187 lines
5.0 KiB
Rust
use nu_test_support::{nu, Outcome};
|
|
use reedline::{
|
|
FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery,
|
|
SqliteBackedHistory,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
struct Test {
|
|
cfg_dir: TempDir,
|
|
}
|
|
|
|
impl Test {
|
|
fn new(history_format: &'static str) -> Self {
|
|
let cfg_dir = tempfile::Builder::new()
|
|
.prefix("history_import_test")
|
|
.tempdir()
|
|
.unwrap();
|
|
// Assigning to $env.config.history.file_format seems to work only in startup
|
|
// configuration.
|
|
std::fs::write(
|
|
cfg_dir.path().join("env.nu"),
|
|
format!("$env.config.history.file_format = {history_format:?}"),
|
|
)
|
|
.unwrap();
|
|
Self { cfg_dir }
|
|
}
|
|
|
|
fn nu(&self, cmd: &'static str) -> Outcome {
|
|
let env = [(
|
|
"XDG_CONFIG_HOME".to_string(),
|
|
self.cfg_dir.path().to_str().unwrap().to_string(),
|
|
)];
|
|
let env_config = self.cfg_dir.path().join("env.nu");
|
|
nu!(envs: env, env_config: env_config, cmd)
|
|
}
|
|
|
|
fn open_plaintext(&self) -> Result<FileBackedHistory, ReedlineError> {
|
|
FileBackedHistory::with_file(100, self.cfg_dir.path().join("nushell").join("history.txt"))
|
|
}
|
|
|
|
fn open_sqlite(&self) -> Result<SqliteBackedHistory, ReedlineError> {
|
|
SqliteBackedHistory::with_file(
|
|
self.cfg_dir.path().join("nushell").join("history.sqlite3"),
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn query_all(history: impl History) -> Result<Vec<HistoryItem>, ReedlineError> {
|
|
history.search(SearchQuery::everything(
|
|
reedline::SearchDirection::Forward,
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn save_all(mut history: impl History, items: Vec<HistoryItem>) -> Result<(), ReedlineError> {
|
|
for item in items {
|
|
history.save(item)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
const EMPTY_ITEM: HistoryItem = HistoryItem {
|
|
command_line: String::new(),
|
|
id: None,
|
|
start_timestamp: None,
|
|
session_id: None,
|
|
hostname: None,
|
|
cwd: None,
|
|
duration: None,
|
|
exit_status: None,
|
|
more_info: None,
|
|
};
|
|
|
|
#[test]
|
|
fn history_import_pipe_string() {
|
|
let test = Test::new("plaintext");
|
|
let outcome = test.nu("echo bar | history import");
|
|
|
|
assert!(outcome.status.success());
|
|
assert_eq!(
|
|
query_all(test.open_plaintext().unwrap()).unwrap(),
|
|
vec![HistoryItem {
|
|
id: Some(HistoryItemId::new(0)),
|
|
command_line: "bar".to_string(),
|
|
..EMPTY_ITEM
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn history_import_pipe_record() {
|
|
let test = Test::new("sqlite");
|
|
let outcome = test.nu("[[item_id command]; [42 some_command]] | history import");
|
|
|
|
assert!(outcome.status.success());
|
|
assert_eq!(
|
|
query_all(test.open_sqlite().unwrap()).unwrap(),
|
|
vec![HistoryItem {
|
|
id: Some(HistoryItemId::new(42)),
|
|
command_line: "some_command".to_string(),
|
|
..EMPTY_ITEM
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn history_import_plain_to_sqlite() {
|
|
let test = Test::new("sqlite");
|
|
save_all(
|
|
test.open_plaintext().unwrap(),
|
|
vec![
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(0)),
|
|
command_line: "foo".to_string(),
|
|
..EMPTY_ITEM
|
|
},
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(1)),
|
|
command_line: "bar".to_string(),
|
|
..EMPTY_ITEM
|
|
},
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
let outcome = test.nu("history import");
|
|
assert!(outcome.status.success());
|
|
assert_eq!(
|
|
query_all(test.open_sqlite().unwrap()).unwrap(),
|
|
vec![
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(0)),
|
|
command_line: "foo".to_string(),
|
|
..EMPTY_ITEM
|
|
},
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(1)),
|
|
command_line: "bar".to_string(),
|
|
..EMPTY_ITEM
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn history_import_sqlite_to_plain() {
|
|
let test = Test::new("plaintext");
|
|
save_all(
|
|
test.open_sqlite().unwrap(),
|
|
vec![
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(0)),
|
|
command_line: "foo".to_string(),
|
|
hostname: Some("host".to_string()),
|
|
..EMPTY_ITEM
|
|
},
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(1)),
|
|
command_line: "bar".to_string(),
|
|
cwd: Some("/home/test".to_string()),
|
|
..EMPTY_ITEM
|
|
},
|
|
],
|
|
)
|
|
.unwrap();
|
|
|
|
let outcome = test.nu("history import");
|
|
assert!(outcome.status.success());
|
|
assert_eq!(
|
|
query_all(test.open_plaintext().unwrap()).unwrap(),
|
|
vec![
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(0)),
|
|
command_line: "foo".to_string(),
|
|
..EMPTY_ITEM
|
|
},
|
|
HistoryItem {
|
|
id: Some(HistoryItemId::new(1)),
|
|
command_line: "bar".to_string(),
|
|
..EMPTY_ITEM
|
|
},
|
|
]
|
|
);
|
|
}
|