mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 18:27:38 +00:00
1903 lines
68 KiB
Rust
1903 lines
68 KiB
Rust
// Copyright 2020 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
extern crate chrono;
|
|
extern crate clap;
|
|
extern crate config;
|
|
|
|
use std::collections::{HashSet, VecDeque};
|
|
use std::ffi::OsString;
|
|
use std::fs;
|
|
use std::fs::OpenOptions;
|
|
use std::io;
|
|
use std::io::{Read, Write};
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
|
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
|
|
use criterion::Criterion;
|
|
|
|
use pest::Parser;
|
|
|
|
use jj_lib::commit::Commit;
|
|
use jj_lib::commit_builder::CommitBuilder;
|
|
use jj_lib::conflicts;
|
|
use jj_lib::dag_walk::{common_ancestor, topo_order_reverse, walk_ancestors};
|
|
use jj_lib::evolution::evolve;
|
|
use jj_lib::evolution::EvolveListener;
|
|
use jj_lib::files;
|
|
use jj_lib::files::DiffLine;
|
|
use jj_lib::op_store::{OpStoreError, OperationId};
|
|
use jj_lib::repo::{ReadonlyRepo, Repo};
|
|
use jj_lib::repo_path::RepoPath;
|
|
use jj_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit};
|
|
use jj_lib::settings::UserSettings;
|
|
use jj_lib::store::{CommitId, Timestamp};
|
|
use jj_lib::store::{StoreError, TreeValue};
|
|
use jj_lib::tree::Tree;
|
|
use jj_lib::trees::TreeValueDiff;
|
|
use jj_lib::working_copy::{CheckoutStats, WorkingCopy};
|
|
|
|
use self::chrono::{FixedOffset, TimeZone, Utc};
|
|
use crate::graphlog::{AsciiGraphDrawer, Edge};
|
|
use crate::styler::{ColorStyler, Styler};
|
|
use crate::template_parser::TemplateParser;
|
|
use crate::templater::Template;
|
|
use crate::ui::Ui;
|
|
use jj_lib::index::{HexPrefix, PrefixResolution};
|
|
use jj_lib::operation::Operation;
|
|
use jj_lib::transaction::Transaction;
|
|
use jj_lib::view::merge_views;
|
|
|
|
enum CommandError {
|
|
UserError(String),
|
|
InternalError(String),
|
|
}
|
|
|
|
fn get_repo(ui: &Ui, matches: &ArgMatches) -> Result<Arc<ReadonlyRepo>, CommandError> {
|
|
let repo_path_str = matches.value_of("repository").unwrap();
|
|
let repo_path = ui.cwd().join(repo_path_str);
|
|
let mut repo = ReadonlyRepo::load(ui.settings(), repo_path);
|
|
if let Some(op_str) = matches.value_of("at_op") {
|
|
let op = resolve_single_op(&repo, op_str)?;
|
|
Arc::get_mut(&mut repo).unwrap().reload_at(&op);
|
|
}
|
|
Ok(repo)
|
|
}
|
|
|
|
fn resolve_commit_id_prefix(
|
|
repo: &ReadonlyRepo,
|
|
prefix: &HexPrefix,
|
|
) -> Result<CommitId, CommandError> {
|
|
let index = repo.index();
|
|
match index.as_composite().resolve_prefix(prefix) {
|
|
PrefixResolution::NoMatch => Err(CommandError::UserError(String::from("No such commit"))),
|
|
PrefixResolution::AmbiguousMatch => {
|
|
Err(CommandError::UserError(String::from("Ambiguous prefix")))
|
|
}
|
|
PrefixResolution::SingleMatch(id) => Ok(id),
|
|
}
|
|
}
|
|
|
|
fn resolve_revision_arg(
|
|
ui: &Ui,
|
|
repo: &mut ReadonlyRepo,
|
|
matches: &ArgMatches,
|
|
) -> Result<Commit, CommandError> {
|
|
resolve_single_rev(ui, repo, matches.value_of("revision").unwrap())
|
|
}
|
|
|
|
fn resolve_single_rev(
|
|
ui: &Ui,
|
|
repo: &mut ReadonlyRepo,
|
|
revision_str: &str,
|
|
) -> Result<Commit, CommandError> {
|
|
if revision_str == "@" {
|
|
let owned_wc = repo.working_copy().clone();
|
|
let wc = owned_wc.lock().unwrap();
|
|
// TODO: Avoid committing every time this function is called.
|
|
Ok(wc.commit(ui.settings(), repo))
|
|
} else if revision_str == "@^" {
|
|
let commit = repo.store().get_commit(repo.view().checkout()).unwrap();
|
|
assert!(commit.is_open());
|
|
let parents = commit.parents();
|
|
Ok(parents[0].clone())
|
|
} else if revision_str == "root" {
|
|
Ok(repo.store().root_commit())
|
|
} else if revision_str.starts_with("desc(") && revision_str.ends_with(')') {
|
|
let needle = revision_str[5..revision_str.len() - 1].to_string();
|
|
let mut matches = vec![];
|
|
let heads: HashSet<Commit> = repo
|
|
.view()
|
|
.heads()
|
|
.map(|commit_id| repo.store().get_commit(commit_id).unwrap())
|
|
.collect();
|
|
let heads = skip_uninteresting_heads(repo, heads);
|
|
for commit in walk_ancestors(heads) {
|
|
if commit.description().contains(&needle) {
|
|
matches.push(commit);
|
|
}
|
|
}
|
|
matches
|
|
.pop()
|
|
.ok_or_else(|| CommandError::UserError(String::from("No matching commit")))
|
|
} else {
|
|
if let Ok(binary_commit_id) = hex::decode(revision_str) {
|
|
let commit_id = CommitId(binary_commit_id);
|
|
match repo.store().get_commit(&commit_id) {
|
|
Ok(commit) => return Ok(commit),
|
|
Err(StoreError::NotFound) => {} // fall through
|
|
Err(err) => {
|
|
return Err(CommandError::InternalError(format!(
|
|
"Failed to read commit: {}",
|
|
err
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
let id = resolve_commit_id_prefix(repo, &HexPrefix::new(revision_str.to_owned()))?;
|
|
Ok(repo.store().get_commit(&id).unwrap())
|
|
}
|
|
}
|
|
|
|
fn rev_arg<'a, 'b>() -> Arg<'a, 'b> {
|
|
Arg::with_name("revision")
|
|
.long("revision")
|
|
.short("r")
|
|
.takes_value(true)
|
|
.default_value("@")
|
|
}
|
|
|
|
fn op_arg<'a, 'b>() -> Arg<'a, 'b> {
|
|
Arg::with_name("operation")
|
|
.long("operation")
|
|
.alias("op")
|
|
.short("o")
|
|
.takes_value(true)
|
|
.default_value("@")
|
|
}
|
|
|
|
fn resolve_single_op(repo: &ReadonlyRepo, op_str: &str) -> Result<Operation, CommandError> {
|
|
let view = repo.view();
|
|
if op_str == "@" {
|
|
Ok(view.base_op_head())
|
|
} else if let Ok(binary_op_id) = hex::decode(op_str) {
|
|
let op_id = OperationId(binary_op_id);
|
|
match view.get_operation(&op_id) {
|
|
Ok(operation) => Ok(operation),
|
|
Err(OpStoreError::NotFound) => Err(CommandError::UserError(format!(
|
|
"Operation id not found: {}",
|
|
op_str
|
|
))),
|
|
Err(err) => Err(CommandError::InternalError(format!(
|
|
"Failed to read commit: {:?}",
|
|
err
|
|
))),
|
|
}
|
|
} else {
|
|
Err(CommandError::UserError(format!(
|
|
"Invalid operation id: {}",
|
|
op_str
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn update_working_copy(
|
|
ui: &mut Ui,
|
|
repo: &mut ReadonlyRepo,
|
|
wc: &WorkingCopy,
|
|
) -> Result<Option<CheckoutStats>, CommandError> {
|
|
repo.reload();
|
|
let old_commit = wc.current_commit();
|
|
let new_commit = repo.store().get_commit(repo.view().checkout()).unwrap();
|
|
if old_commit == new_commit {
|
|
return Ok(None);
|
|
}
|
|
ui.write("leaving: ");
|
|
ui.write_commit_summary(repo, &old_commit);
|
|
ui.write("\n");
|
|
// TODO: CheckoutError::ConcurrentCheckout should probably just result in a
|
|
// warning for most commands (but be an error for the checkout command)
|
|
let stats = wc.check_out(new_commit.clone()).map_err(|err| {
|
|
CommandError::InternalError(format!(
|
|
"Failed to check out commit {}: {}",
|
|
new_commit.id().hex(),
|
|
err
|
|
))
|
|
})?;
|
|
ui.write("now at: ");
|
|
ui.write_commit_summary(repo, &new_commit);
|
|
ui.write("\n");
|
|
Ok(Some(stats))
|
|
}
|
|
|
|
fn update_checkout_after_rewrite(ui: &mut Ui, tx: &mut Transaction) {
|
|
// TODO: Perhaps this method should be in Transaction.
|
|
let repo = tx.as_repo();
|
|
let new_checkout_candidates = repo.evolution().new_parent(repo.view().checkout());
|
|
if new_checkout_candidates.is_empty() {
|
|
return;
|
|
}
|
|
// Filter out heads that already existed.
|
|
// TODO: Filter out *commits* that already existed (so we get updated to an
|
|
// appropriate new non-head)
|
|
let old_heads: HashSet<_> = tx.base_repo().view().heads().cloned().collect();
|
|
let new_checkout_candidates: HashSet<_> = new_checkout_candidates
|
|
.difference(&old_heads)
|
|
.cloned()
|
|
.collect();
|
|
if new_checkout_candidates.is_empty() {
|
|
return;
|
|
}
|
|
if new_checkout_candidates.len() > 1 {
|
|
ui.write(
|
|
"There are several candidates for updating the checkout to -- picking arbitrarily\n",
|
|
);
|
|
}
|
|
let new_checkout = new_checkout_candidates.iter().min().unwrap();
|
|
let new_commit = repo.store().get_commit(new_checkout).unwrap();
|
|
tx.check_out(ui.settings(), &new_commit);
|
|
}
|
|
|
|
fn get_app<'a, 'b>() -> App<'a, 'b> {
|
|
App::new("Jujube")
|
|
.global_setting(clap::AppSettings::ColoredHelp)
|
|
.version("0.0.1")
|
|
.author("Martin von Zweigbergk <martinvonz@google.com>")
|
|
.about("My source control tool")
|
|
.arg(
|
|
Arg::with_name("repository")
|
|
.long("repository")
|
|
.short("R")
|
|
.takes_value(true)
|
|
.default_value("."),
|
|
)
|
|
.arg(Arg::with_name("at_op").long("at-operation").alias("at-op").takes_value(true))
|
|
.subcommand(
|
|
SubCommand::with_name("init")
|
|
.about("initialize a repo")
|
|
.arg(Arg::with_name("destination").index(1).default_value("."))
|
|
.arg(
|
|
Arg::with_name("git-store")
|
|
.long("git-store")
|
|
.takes_value(true)
|
|
.help("path to a .git backing store"),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("checkout")
|
|
.alias("co")
|
|
.about("update the working copy to another commit")
|
|
.arg(Arg::with_name("revision").index(1).required(true)),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("files")
|
|
.about("list files")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("diff")
|
|
.about("show modified files")
|
|
.arg(
|
|
Arg::with_name("summary")
|
|
.long("summary")
|
|
.short("s")
|
|
.help("show only the diff type (modified/added/removed)"),
|
|
)
|
|
.arg(Arg::with_name("revision")
|
|
.long("revision")
|
|
.short("r")
|
|
.takes_value(true)
|
|
)
|
|
.arg(Arg::with_name("from").long("from").takes_value(true))
|
|
.arg(Arg::with_name("to").long("to").takes_value(true)),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("status")
|
|
.alias("st")
|
|
.about("show repo status"),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("log")
|
|
.about("show commit history")
|
|
.arg(
|
|
Arg::with_name("template")
|
|
.long("template")
|
|
.short("T")
|
|
.takes_value(true),
|
|
)
|
|
.arg(Arg::with_name("all").long("all"))
|
|
.arg(Arg::with_name("no-graph").long("no-graph")),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("obslog")
|
|
.about("show how a commit has evolved")
|
|
.arg(rev_arg())
|
|
.arg(
|
|
Arg::with_name("template")
|
|
.long("template")
|
|
.short("T")
|
|
.takes_value(true),
|
|
)
|
|
.arg(Arg::with_name("no-graph").long("no-graph")),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("describe")
|
|
.about("edit the commit description")
|
|
.arg(rev_arg())
|
|
.arg(Arg::with_name("text").long("text").takes_value(true))
|
|
.arg(Arg::with_name("stdin").long("stdin")),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("close")
|
|
.about("mark a commit closed, making new work go into a new commit")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("open")
|
|
.about("mark a commit open, making new work be added to it")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("duplicate")
|
|
.about("create a copy of the commit with a new change id")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("prune")
|
|
.about("create an empty successor of a commit")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("new")
|
|
.about("create a new, empty commit")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("squash")
|
|
.about("squash a commit into its parent")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("discard")
|
|
.about("discard a commit (and its descendants)")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("restore")
|
|
.about("restore paths from another revision")
|
|
.arg(
|
|
Arg::with_name("source")
|
|
.long("source")
|
|
.short("s")
|
|
.takes_value(true)
|
|
.default_value("@^"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("destination")
|
|
.long("destination")
|
|
.short("d")
|
|
.takes_value(true)
|
|
.default_value("@"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("paths")
|
|
.index(1)
|
|
.required(true)
|
|
.multiple(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("merge")
|
|
.about("merge work from multiple branches")
|
|
.arg(
|
|
Arg::with_name("revisions")
|
|
.index(1)
|
|
.required(true)
|
|
.multiple(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("rebase")
|
|
.about("move a commit to a different parent")
|
|
.arg(rev_arg())
|
|
.arg(
|
|
Arg::with_name("destination")
|
|
.long("destination")
|
|
.short("d")
|
|
.takes_value(true)
|
|
.required(true)
|
|
.multiple(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("backout")
|
|
.about("apply the reverse of a commit on top of another commit")
|
|
.arg(rev_arg())
|
|
.arg(
|
|
Arg::with_name("destination")
|
|
.long("destination")
|
|
.short("d")
|
|
.takes_value(true)
|
|
.default_value("@")
|
|
.multiple(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("evolve").about("resolve problems with the repo's meta-history"),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("operation")
|
|
.alias("op")
|
|
.about("commands for working with the operation log")
|
|
.subcommand(SubCommand::with_name("log").about("show the operation log"))
|
|
.subcommand(
|
|
SubCommand::with_name("undo")
|
|
.about("undo an operation")
|
|
.arg(op_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("restore")
|
|
.about("restore to the state at an operation")
|
|
.arg(op_arg()),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("bench")
|
|
.about("commands for benchmarking internal operations")
|
|
.subcommand(
|
|
SubCommand::with_name("commonancestors")
|
|
.about("finds the common ancestor(s) of a set of commits")
|
|
.arg(Arg::with_name("revision1").index(1).required(true))
|
|
.arg(Arg::with_name("revision2").index(2).required(true)),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("isancestor")
|
|
.about("checks if the first commit is an ancestor of the second commit")
|
|
.arg(Arg::with_name("ancestor").index(1).required(true))
|
|
.arg(Arg::with_name("descendant").index(2).required(true)),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("walkrevs")
|
|
.about("walks revisions that are ancestors of the second argument but not ancestors of the first")
|
|
.arg(Arg::with_name("unwanted").index(1).required(true))
|
|
.arg(Arg::with_name("wanted").index(2).required(true)),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("resolveprefix")
|
|
.about("resolve a commit id prefix")
|
|
.arg(Arg::with_name("prefix").index(1).required(true)),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("debug")
|
|
.about("low-level commands not intended for users")
|
|
.subcommand(
|
|
SubCommand::with_name("resolverev")
|
|
.about("resolves a revision identifier to its full id")
|
|
.arg(rev_arg()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("workingcopy")
|
|
.about("show information about the working copy state"),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("writeworkingcopy")
|
|
.about("write a tree from the working copy state"),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("template")
|
|
.about("parse a template")
|
|
.arg(Arg::with_name("template").index(1).required(true)),
|
|
)
|
|
.subcommand(SubCommand::with_name("index").about("show commit index stats"))
|
|
.subcommand(SubCommand::with_name("reindex").about("rebuild commit index")),
|
|
)
|
|
}
|
|
|
|
fn cmd_init(
|
|
ui: &mut Ui,
|
|
_matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let wc_path_str = sub_matches.value_of("destination").unwrap();
|
|
let wc_path = ui.cwd().join(wc_path_str);
|
|
if wc_path.exists() {
|
|
assert!(wc_path.is_dir());
|
|
} else {
|
|
fs::create_dir(&wc_path).unwrap();
|
|
}
|
|
|
|
let repo;
|
|
if let Some(git_store_str) = sub_matches.value_of("git-store") {
|
|
let git_store_path = ui.cwd().join(git_store_str);
|
|
repo = ReadonlyRepo::init_git(ui.settings(), wc_path, git_store_path);
|
|
} else {
|
|
repo = ReadonlyRepo::init_local(ui.settings(), wc_path);
|
|
}
|
|
writeln!(ui, "Initialized repo in {:?}", repo.working_copy_path());
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_checkout(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let new_commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let wc = owned_wc.lock().unwrap();
|
|
wc.commit(ui.settings(), mut_repo);
|
|
let mut tx = repo.start_transaction(&format!("check out commit {}", new_commit.id().hex()));
|
|
tx.check_out(ui.settings(), &new_commit);
|
|
tx.commit();
|
|
let stats = update_working_copy(ui, Arc::get_mut(&mut repo).unwrap(), &wc)?;
|
|
match stats {
|
|
None => ui.write("already on that commit\n"),
|
|
Some(stats) => writeln!(
|
|
ui,
|
|
"added {} files, modified {} files, removed {} files",
|
|
stats.added_files, stats.updated_files, stats.removed_files
|
|
),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_files(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
for (name, _value) in commit.tree().entries() {
|
|
writeln!(ui, "{}", name.to_internal_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_diff(left: &[u8], right: &[u8], styler: &mut dyn Styler) {
|
|
let num_context_lines = 3;
|
|
let mut context = VecDeque::new();
|
|
// Have we printed "..." for any skipped context?
|
|
let mut skipped_context = false;
|
|
// Are the lines in `context` to be printed before the next modified line?
|
|
let mut context_before = true;
|
|
files::diff(left, right, &mut |diff_line| {
|
|
if diff_line.is_unmodified() {
|
|
context.push_back(diff_line.clone());
|
|
if context.len() > num_context_lines {
|
|
if context_before {
|
|
context.pop_front();
|
|
} else {
|
|
context.pop_back();
|
|
}
|
|
if !context_before {
|
|
for line in &context {
|
|
print_diff_line(styler, line);
|
|
}
|
|
context.clear();
|
|
context_before = true;
|
|
}
|
|
if !skipped_context {
|
|
styler.write_bytes(b" ...\n");
|
|
skipped_context = true;
|
|
}
|
|
}
|
|
} else {
|
|
if context_before {
|
|
for line in &context {
|
|
print_diff_line(styler, line);
|
|
}
|
|
}
|
|
context.clear();
|
|
print_diff_line(styler, diff_line);
|
|
context_before = false;
|
|
skipped_context = false;
|
|
}
|
|
});
|
|
if !context_before {
|
|
for line in &context {
|
|
print_diff_line(styler, line);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print_diff_line(styler: &mut dyn Styler, diff_line: &DiffLine) {
|
|
if diff_line.has_left_content {
|
|
styler.add_label(String::from("left"));
|
|
styler.write_bytes(format!("{:>4}", diff_line.left_line_number).as_bytes());
|
|
styler.remove_label();
|
|
styler.write_bytes(b" ");
|
|
} else {
|
|
styler.write_bytes(b" ");
|
|
}
|
|
if diff_line.has_right_content {
|
|
styler.add_label(String::from("right"));
|
|
styler.write_bytes(format!("{:>4}", diff_line.right_line_number).as_bytes());
|
|
styler.remove_label();
|
|
styler.write_bytes(b": ");
|
|
} else {
|
|
styler.write_bytes(b" : ");
|
|
}
|
|
for hunk in &diff_line.hunks {
|
|
match hunk {
|
|
files::DiffHunk::Unmodified(data) => {
|
|
styler.write_bytes(data.as_slice());
|
|
}
|
|
files::DiffHunk::Removed(data) => {
|
|
styler.add_label(String::from("left"));
|
|
styler.write_bytes(data.as_slice());
|
|
styler.remove_label();
|
|
}
|
|
files::DiffHunk::Added(data) => {
|
|
styler.add_label(String::from("right"));
|
|
styler.write_bytes(data.as_slice());
|
|
styler.remove_label();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cmd_diff(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
if sub_matches.is_present("revision")
|
|
&& (sub_matches.is_present("from") || sub_matches.is_present("to"))
|
|
{
|
|
return Err(CommandError::UserError(String::from(
|
|
"--revision cannot be used with --from or --to",
|
|
)));
|
|
}
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
if sub_matches.is_present("from") || sub_matches.is_present("to") {}
|
|
let from_tree;
|
|
let to_tree;
|
|
if sub_matches.is_present("from") || sub_matches.is_present("to") {
|
|
from_tree =
|
|
resolve_single_rev(ui, mut_repo, sub_matches.value_of("from").unwrap_or("@"))?.tree();
|
|
to_tree =
|
|
resolve_single_rev(ui, mut_repo, sub_matches.value_of("to").unwrap_or("@"))?.tree();
|
|
} else {
|
|
let commit = resolve_single_rev(
|
|
ui,
|
|
mut_repo,
|
|
sub_matches.value_of("revision").unwrap_or("@"),
|
|
)?;
|
|
let parents = commit.parents();
|
|
from_tree = merge_commit_trees(repo.store(), &parents);
|
|
to_tree = commit.tree()
|
|
}
|
|
if sub_matches.is_present("summary") {
|
|
show_diff_summary(ui, &from_tree, &to_tree);
|
|
} else {
|
|
let mut styler = ui.styler();
|
|
styler.add_label(String::from("diff"));
|
|
from_tree.diff(&to_tree, &mut |path, diff| match diff {
|
|
TreeValueDiff::Added(TreeValue::Normal {
|
|
id,
|
|
executable: false,
|
|
}) => {
|
|
styler.add_label(String::from("header"));
|
|
styler.write_str(&format!("added file {}:\n", path.to_internal_string()));
|
|
styler.remove_label();
|
|
|
|
let mut file_reader = repo.store().read_file(path, id).unwrap();
|
|
styler.write_from_reader(&mut file_reader);
|
|
}
|
|
TreeValueDiff::Modified(
|
|
TreeValue::Normal {
|
|
id: id_left,
|
|
executable: false,
|
|
},
|
|
TreeValue::Normal {
|
|
id: id_right,
|
|
executable: false,
|
|
},
|
|
) => {
|
|
styler.add_label(String::from("header"));
|
|
styler.write_str(&format!("modified file {}:\n", path.to_internal_string()));
|
|
styler.remove_label();
|
|
|
|
let mut file_reader_left = repo.store().read_file(path, id_left).unwrap();
|
|
let mut buffer_left = vec![];
|
|
file_reader_left.read_to_end(&mut buffer_left).unwrap();
|
|
let mut file_reader_right = repo.store().read_file(path, id_right).unwrap();
|
|
let mut buffer_right = vec![];
|
|
file_reader_right.read_to_end(&mut buffer_right).unwrap();
|
|
|
|
print_diff(
|
|
buffer_left.as_slice(),
|
|
buffer_right.as_slice(),
|
|
styler.as_mut(),
|
|
);
|
|
}
|
|
TreeValueDiff::Modified(
|
|
TreeValue::Conflict(id_left),
|
|
TreeValue::Normal {
|
|
id: id_right,
|
|
executable: false,
|
|
},
|
|
) => {
|
|
styler.add_label(String::from("header"));
|
|
styler.write_str(&format!(
|
|
"resolved conflict in file {}:\n",
|
|
path.to_internal_string()
|
|
));
|
|
styler.remove_label();
|
|
|
|
let conflict_left = repo.store().read_conflict(id_left).unwrap();
|
|
let mut buffer_left = vec![];
|
|
conflicts::materialize_conflict(
|
|
repo.store(),
|
|
&path.to_repo_path(),
|
|
&conflict_left,
|
|
&mut buffer_left,
|
|
);
|
|
let mut file_reader_right = repo.store().read_file(path, id_right).unwrap();
|
|
let mut buffer_right = vec![];
|
|
file_reader_right.read_to_end(&mut buffer_right).unwrap();
|
|
|
|
print_diff(
|
|
buffer_left.as_slice(),
|
|
buffer_right.as_slice(),
|
|
styler.as_mut(),
|
|
);
|
|
}
|
|
TreeValueDiff::Modified(
|
|
TreeValue::Normal {
|
|
id: id_left,
|
|
executable: false,
|
|
},
|
|
TreeValue::Conflict(id_right),
|
|
) => {
|
|
styler.add_label(String::from("header"));
|
|
styler.write_str(&format!(
|
|
"new conflict in file {}:\n",
|
|
path.to_internal_string()
|
|
));
|
|
styler.remove_label();
|
|
|
|
let mut file_reader_left = repo.store().read_file(path, id_left).unwrap();
|
|
let mut buffer_left = vec![];
|
|
file_reader_left.read_to_end(&mut buffer_left).unwrap();
|
|
let conflict_right = repo.store().read_conflict(id_right).unwrap();
|
|
let mut buffer_right = vec![];
|
|
conflicts::materialize_conflict(
|
|
repo.store(),
|
|
&path.to_repo_path(),
|
|
&conflict_right,
|
|
&mut buffer_right,
|
|
);
|
|
|
|
print_diff(
|
|
buffer_left.as_slice(),
|
|
buffer_right.as_slice(),
|
|
styler.as_mut(),
|
|
);
|
|
}
|
|
TreeValueDiff::Removed(TreeValue::Normal {
|
|
id,
|
|
executable: false,
|
|
}) => {
|
|
styler.add_label(String::from("header"));
|
|
styler.write_str(&format!("removed file {}:\n", path.to_internal_string()));
|
|
styler.remove_label();
|
|
|
|
let mut file_reader = repo.store().read_file(path, id).unwrap();
|
|
styler.write_from_reader(&mut file_reader);
|
|
}
|
|
other => {
|
|
writeln!(
|
|
styler,
|
|
"unhandled diff case in path {:?}: {:?}",
|
|
path, other
|
|
)
|
|
.unwrap();
|
|
}
|
|
});
|
|
styler.remove_label();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn show_diff_summary(ui: &mut Ui, from: &Tree, to: &Tree) {
|
|
let summary = from.diff_summary(&to);
|
|
for file in summary.modified {
|
|
writeln!(ui, "M {}", file.to_internal_string());
|
|
}
|
|
for file in summary.added {
|
|
writeln!(ui, "A {}", file.to_internal_string());
|
|
}
|
|
for file in summary.removed {
|
|
writeln!(ui, "R {}", file.to_internal_string());
|
|
}
|
|
}
|
|
|
|
fn cmd_status(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
_sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let wc = owned_wc.lock().unwrap();
|
|
let commit = wc.commit(ui.settings(), mut_repo);
|
|
ui.write("Working copy : ");
|
|
ui.write_commit_summary(repo.as_ref(), &commit);
|
|
ui.write("\n");
|
|
ui.write("Parent commit: ");
|
|
ui.write_commit_summary(repo.as_ref(), &commit.parents()[0]);
|
|
ui.write("\n");
|
|
ui.write("Diff summary:\n");
|
|
show_diff_summary(ui, &commit.parents()[0].tree(), &commit.tree());
|
|
Ok(())
|
|
}
|
|
|
|
fn log_template(settings: &UserSettings) -> String {
|
|
let default_template = r#"
|
|
label(if(open, "open"),
|
|
"commit: " commit_id "\n"
|
|
"change: " change_id "\n"
|
|
"author: " author.name() " <" author.email() ">\n"
|
|
"committer: " committer.name() " <" committer.email() ">\n"
|
|
"open: " open "\n"
|
|
"pruned: " pruned "\n"
|
|
"obsolete: " obsolete "\n"
|
|
"orphan: " orphan "\n"
|
|
"divergent: " divergent "\n"
|
|
"has conflict: " conflict "\n"
|
|
description "\n"
|
|
)"#;
|
|
settings
|
|
.config()
|
|
.get_str("template.log")
|
|
.unwrap_or_else(|_| String::from(default_template))
|
|
}
|
|
|
|
fn graph_log_template(settings: &UserSettings) -> String {
|
|
// TODO: define a method on boolean values, so we can get auto-coloring
|
|
// with e.g. `obsolete.then("obsolete")`
|
|
let default_template = r#"
|
|
if(current_checkout, "<-- ")
|
|
label(if(open, "open"),
|
|
commit_id.short()
|
|
" " change_id.short()
|
|
" " author.email()
|
|
" " committer.email()
|
|
if(pruned, label("pruned", " pruned"))
|
|
if(obsolete, label("obsolete", " obsolete"))
|
|
if(orphan, label("orphan", " orphan"))
|
|
if(divergent, label("divergent", " divergent"))
|
|
if(conflict, label("conflict", " conflict"))
|
|
"\n"
|
|
description.first_line()
|
|
"\n"
|
|
)"#;
|
|
settings
|
|
.config()
|
|
.get_str("template.log.graph")
|
|
.unwrap_or_else(|_| String::from(default_template))
|
|
}
|
|
|
|
fn skip_uninteresting_heads(repo: &ReadonlyRepo, heads: HashSet<Commit>) -> HashSet<Commit> {
|
|
let checkout_id = repo.view().checkout().clone();
|
|
let mut result = HashSet::new();
|
|
let mut work: Vec<_> = heads.into_iter().collect();
|
|
let evolution = repo.evolution();
|
|
while !work.is_empty() {
|
|
let commit = work.pop().unwrap();
|
|
if result.contains(&commit) {
|
|
continue;
|
|
}
|
|
if (!commit.is_pruned() && !evolution.is_obsolete(commit.id()))
|
|
|| commit.id() == &checkout_id
|
|
{
|
|
result.insert(commit);
|
|
} else {
|
|
for parent in commit.parents() {
|
|
work.push(parent);
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn cmd_log(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
|
|
let use_graph = !sub_matches.is_present("no-graph");
|
|
if use_graph {
|
|
// Commit so the latest working copy is reflected in the visible heads
|
|
owned_wc.lock().unwrap().commit(ui.settings(), mut_repo);
|
|
}
|
|
|
|
let template_string = match sub_matches.value_of("template") {
|
|
Some(value) => value.to_string(),
|
|
None => {
|
|
if use_graph {
|
|
graph_log_template(ui.settings())
|
|
} else {
|
|
log_template(ui.settings())
|
|
}
|
|
}
|
|
};
|
|
let template = crate::template_parser::parse_commit_template(repo.as_ref(), &template_string);
|
|
|
|
let mut styler = ui.styler();
|
|
let mut styler = styler.as_mut();
|
|
styler.add_label(String::from("log"));
|
|
|
|
let mut heads: HashSet<_> = repo
|
|
.view()
|
|
.heads()
|
|
.map(|id| repo.store().get_commit(id).unwrap())
|
|
.collect();
|
|
if !sub_matches.is_present("all") {
|
|
heads = skip_uninteresting_heads(&repo, heads);
|
|
};
|
|
let mut heads: Vec<_> = heads.into_iter().collect();
|
|
heads.sort();
|
|
|
|
let commits = topo_order_reverse(
|
|
heads,
|
|
Box::new(|commit: &Commit| commit.id().clone()),
|
|
Box::new(|commit: &Commit| commit.parents()),
|
|
);
|
|
if use_graph {
|
|
let mut graph = AsciiGraphDrawer::new(&mut styler);
|
|
for commit in commits {
|
|
let mut edges = vec![];
|
|
for parent in commit.parents() {
|
|
edges.push(Edge::direct(parent.id().clone()));
|
|
}
|
|
let mut buffer = vec![];
|
|
// TODO: only use color if requested
|
|
{
|
|
let writer = Box::new(&mut buffer);
|
|
let mut styler = ColorStyler::new(writer, ui.settings());
|
|
template.format(&commit, &mut styler);
|
|
}
|
|
if !buffer.ends_with(b"\n") {
|
|
buffer.push(b'\n');
|
|
}
|
|
graph.add_node(commit.id(), &edges, b"o", &buffer);
|
|
}
|
|
} else {
|
|
for commit in commits {
|
|
template.format(&commit, styler);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_obslog(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
|
|
let use_graph = !sub_matches.is_present("no-graph");
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let start_commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
|
|
let template_string = match sub_matches.value_of("template") {
|
|
Some(value) => value.to_string(),
|
|
None => {
|
|
if use_graph {
|
|
graph_log_template(ui.settings())
|
|
} else {
|
|
log_template(ui.settings())
|
|
}
|
|
}
|
|
};
|
|
let template = crate::template_parser::parse_commit_template(repo.as_ref(), &template_string);
|
|
|
|
let mut styler = ui.styler();
|
|
let mut styler = styler.as_mut();
|
|
styler.add_label(String::from("log"));
|
|
|
|
let commits = topo_order_reverse(
|
|
vec![start_commit],
|
|
Box::new(|commit: &Commit| commit.id().clone()),
|
|
Box::new(|commit: &Commit| commit.predecessors()),
|
|
);
|
|
if use_graph {
|
|
let mut graph = AsciiGraphDrawer::new(&mut styler);
|
|
for commit in commits {
|
|
let mut edges = vec![];
|
|
for predecessor in commit.predecessors() {
|
|
edges.push(Edge::direct(predecessor.id().clone()));
|
|
}
|
|
let mut buffer = vec![];
|
|
// TODO: only use color if requested
|
|
{
|
|
let writer = Box::new(&mut buffer);
|
|
let mut styler = ColorStyler::new(writer, ui.settings());
|
|
template.format(&commit, &mut styler);
|
|
}
|
|
if !buffer.ends_with(b"\n") {
|
|
buffer.push(b'\n');
|
|
}
|
|
graph.add_node(commit.id(), &edges, b"o", &buffer);
|
|
}
|
|
} else {
|
|
for commit in commits {
|
|
template.format(&commit, styler);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn edit_description(repo: &ReadonlyRepo, commit: &Commit) -> String {
|
|
// TODO: Where should this file live? The current location prevents two
|
|
// concurrent `jj describe` calls.
|
|
let description_file_path = repo.repo_path().join("description");
|
|
{
|
|
let mut description_file = OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(&description_file_path)
|
|
.unwrap_or_else(|_| panic!("failed to open {:?} for write", &description_file_path));
|
|
description_file
|
|
.write_all(commit.description().as_bytes())
|
|
.unwrap();
|
|
}
|
|
|
|
let exit_status = Command::new("pico")
|
|
.arg(&description_file_path)
|
|
.status()
|
|
.expect("failed to run editor");
|
|
if !exit_status.success() {
|
|
panic!("failed to run editor");
|
|
}
|
|
|
|
let mut description_file = OpenOptions::new()
|
|
.read(true)
|
|
.open(&description_file_path)
|
|
.unwrap_or_else(|_| panic!("failed to open {:?} for read", &description_file_path));
|
|
let mut buf = vec![];
|
|
description_file.read_to_end(&mut buf).unwrap();
|
|
String::from_utf8(buf).unwrap()
|
|
}
|
|
|
|
fn cmd_describe(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let description;
|
|
if sub_matches.is_present("stdin") {
|
|
let mut buffer = String::new();
|
|
io::stdin().read_to_string(&mut buffer).unwrap();
|
|
description = buffer;
|
|
} else if sub_matches.is_present("text") {
|
|
description = sub_matches.value_of("text").unwrap().to_owned()
|
|
} else {
|
|
description = edit_description(&repo, &commit);
|
|
}
|
|
let mut tx = repo.start_transaction(&format!("describe commit {}", commit.id().hex()));
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &commit)
|
|
.set_description(description)
|
|
.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_open(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut tx = repo.start_transaction(&format!("open commit {}", commit.id().hex()));
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &commit)
|
|
.set_open(true)
|
|
.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_close(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut commit_builder =
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &commit).set_open(false);
|
|
if commit.description().is_empty() {
|
|
let description = edit_description(&repo, &commit);
|
|
commit_builder = commit_builder.set_description(description);
|
|
}
|
|
let mut tx = repo.start_transaction(&format!("close commit {}", commit.id().hex()));
|
|
commit_builder.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_duplicate(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let predecessor = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut tx = repo.start_transaction(&format!("duplicate commit {}", predecessor.id().hex()));
|
|
let new_commit = CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &predecessor)
|
|
.generate_new_change_id()
|
|
.write_to_transaction(&mut tx);
|
|
ui.write("created: ");
|
|
ui.write_commit_summary(tx.as_repo(), &new_commit);
|
|
ui.write("\n");
|
|
tx.commit();
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_prune(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let predecessor = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
if predecessor.id() == repo.store().root_commit_id() {
|
|
return Err(CommandError::UserError(String::from(
|
|
"Cannot prune the root commit",
|
|
)));
|
|
}
|
|
let mut tx = repo.start_transaction(&format!("prune commit {}", predecessor.id().hex()));
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &predecessor)
|
|
.set_pruned(true)
|
|
.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_new(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let parent = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let commit_builder = CommitBuilder::for_open_commit(
|
|
ui.settings(),
|
|
repo.store(),
|
|
parent.id().clone(),
|
|
parent.tree().id().clone(),
|
|
);
|
|
let mut tx = repo.start_transaction("new empty commit");
|
|
let new_commit = commit_builder.write_to_transaction(&mut tx);
|
|
if tx.as_repo().view().checkout() == parent.id() {
|
|
tx.check_out(ui.settings(), &new_commit);
|
|
}
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_squash(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let parents = commit.parents();
|
|
if parents.len() != 1 {
|
|
return Err(CommandError::UserError(String::from(
|
|
"Cannot squash merge commits",
|
|
)));
|
|
}
|
|
let parent = &parents[0];
|
|
if parent.id() == repo.store().root_commit_id() {
|
|
return Err(CommandError::UserError(String::from(
|
|
"Cannot squash into the root commit",
|
|
)));
|
|
}
|
|
let mut tx = repo.start_transaction(&format!("squash commit {}", commit.id().hex()));
|
|
let squashed_commit = CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &parent)
|
|
.set_tree(commit.tree().id().clone())
|
|
.set_predecessors(vec![parent.id().clone(), commit.id().clone()])
|
|
.write_to_transaction(&mut tx);
|
|
// Commit the remainder on top of the new commit (always empty in the
|
|
// non-interactive case), so the squashed-in commit becomes obsolete, and so
|
|
// descendants evolve correctly.
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &commit)
|
|
.set_parents(vec![squashed_commit.id().clone()])
|
|
.set_pruned(true)
|
|
.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_discard(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut tx = repo.start_transaction(&format!("discard commit {}", commit.id().hex()));
|
|
tx.remove_head(&commit);
|
|
// TODO: also remove descendants
|
|
tx.commit();
|
|
// TODO: check out parent/ancestor if the current commit got hidden
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_restore(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let source_commit = resolve_single_rev(ui, mut_repo, sub_matches.value_of("source").unwrap())?;
|
|
let destination_commit =
|
|
resolve_single_rev(ui, mut_repo, sub_matches.value_of("destination").unwrap())?;
|
|
let paths = sub_matches.values_of("paths").unwrap();
|
|
let mut tree_builder = repo
|
|
.store()
|
|
.tree_builder(destination_commit.tree().id().clone());
|
|
for path in paths {
|
|
let repo_path = RepoPath::from(path);
|
|
match source_commit.tree().path_value(&repo_path) {
|
|
Some(value) => {
|
|
tree_builder.set(repo_path, value);
|
|
}
|
|
None => {
|
|
tree_builder.remove(repo_path);
|
|
}
|
|
}
|
|
}
|
|
let tree_id = tree_builder.write_tree();
|
|
if &tree_id == destination_commit.tree().id() {
|
|
ui.write("Nothing changed.\n");
|
|
} else {
|
|
let mut tx = repo.start_transaction(&format!(
|
|
"restore into commit {}",
|
|
destination_commit.id().hex()
|
|
));
|
|
let new_commit =
|
|
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &destination_commit)
|
|
.set_tree(tree_id)
|
|
.write_to_transaction(&mut tx);
|
|
ui.write("Created ");
|
|
ui.write_commit_summary(tx.as_repo(), &new_commit);
|
|
ui.write("\n");
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_merge(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let revision_args = sub_matches.values_of("revisions").unwrap();
|
|
if revision_args.len() < 2 {
|
|
return Err(CommandError::UserError(String::from(
|
|
"Merge requires at least two revisions",
|
|
)));
|
|
}
|
|
let mut commits = vec![];
|
|
let mut parent_ids = vec![];
|
|
for revision_arg in revision_args {
|
|
let commit = resolve_single_rev(ui, mut_repo, revision_arg)?;
|
|
parent_ids.push(commit.id().clone());
|
|
commits.push(commit);
|
|
}
|
|
let merged_tree = merge_commit_trees(repo.store(), &commits);
|
|
let mut tx = repo.start_transaction("merge commits");
|
|
CommitBuilder::for_new_commit(ui.settings(), repo.store(), merged_tree.id().clone())
|
|
.set_parents(parent_ids)
|
|
.set_open(false)
|
|
.write_to_transaction(&mut tx);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_rebase(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit_to_rebase = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut parents = vec![];
|
|
for revision_str in sub_matches.values_of("destination").unwrap() {
|
|
parents.push(resolve_single_rev(ui, mut_repo, revision_str)?);
|
|
}
|
|
let mut tx = repo.start_transaction(&format!("rebase commit {}", commit_to_rebase.id().hex()));
|
|
rebase_commit(ui.settings(), &mut tx, &commit_to_rebase, &parents);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_backout(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit_to_back_out = resolve_revision_arg(ui, mut_repo, sub_matches)?;
|
|
let mut parents = vec![];
|
|
for revision_str in sub_matches.values_of("destination").unwrap() {
|
|
parents.push(resolve_single_rev(ui, mut_repo, revision_str)?);
|
|
}
|
|
let mut tx = repo.start_transaction(&format!(
|
|
"back out commit {}",
|
|
commit_to_back_out.id().hex()
|
|
));
|
|
back_out_commit(ui.settings(), &mut tx, &commit_to_back_out, &parents);
|
|
update_checkout_after_rewrite(ui, &mut tx);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_evolve<'s>(
|
|
ui: &mut Ui<'s>,
|
|
matches: &ArgMatches,
|
|
_sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let repo = get_repo(ui, &matches)?;
|
|
|
|
struct Listener<'a, 's, 'r> {
|
|
ui: &'a mut Ui<'s>,
|
|
repo: &'r dyn Repo,
|
|
};
|
|
|
|
impl<'a, 's, 'r> EvolveListener for Listener<'a, 's, 'r> {
|
|
fn orphan_evolved(&mut self, orphan: &Commit, new_commit: &Commit) {
|
|
self.ui.write("Resolving orphan: ");
|
|
self.ui.write_commit_summary(self.repo, &orphan);
|
|
self.ui.write("\n");
|
|
self.ui.write("Resolved as: ");
|
|
self.ui.write_commit_summary(self.repo, &new_commit);
|
|
self.ui.write("\n");
|
|
}
|
|
|
|
fn orphan_target_ambiguous(&mut self, orphan: &Commit) {
|
|
self.ui
|
|
.write("Skipping orphan with ambiguous new parents: ");
|
|
self.ui.write_commit_summary(self.repo, &orphan);
|
|
self.ui.write("\n");
|
|
}
|
|
|
|
fn divergent_resolved(&mut self, sources: &[Commit], resolved: &Commit) {
|
|
self.ui.write("Resolving divergent commits:\n");
|
|
for source in sources {
|
|
self.ui.write(" ");
|
|
self.ui.write_commit_summary(self.repo, &source);
|
|
self.ui.write("\n");
|
|
}
|
|
self.ui.write("Resolved as: ");
|
|
self.ui.write_commit_summary(self.repo, &resolved);
|
|
self.ui.write("\n");
|
|
}
|
|
|
|
fn divergent_no_common_predecessor(&mut self, commit1: &Commit, commit2: &Commit) {
|
|
self.ui
|
|
.write("Skipping divergent commits with no common predecessor:\n");
|
|
self.ui.write(" ");
|
|
self.ui.write_commit_summary(self.repo, &commit1);
|
|
self.ui.write("\n");
|
|
self.ui.write(" ");
|
|
self.ui.write_commit_summary(self.repo, &commit2);
|
|
self.ui.write("\n");
|
|
}
|
|
}
|
|
|
|
// TODO: This clone is unnecessary. Maybe ui.write() etc should not require a
|
|
// mutable borrow? But the mutable borrow might be useful for making sure we
|
|
// have only one Ui instance we write to across threads?
|
|
let user_settings = ui.settings().clone();
|
|
let mut listener = Listener {
|
|
ui,
|
|
// TODO: This should be using tx.as_repo() so the templater sees the updated state, but
|
|
// we can't do that because we let evolution::evolve() mutably borrow the Transaction.
|
|
repo: repo.as_ref(),
|
|
};
|
|
let mut tx = repo.start_transaction("evolve");
|
|
evolve(&user_settings, &mut tx, &mut listener);
|
|
// TODO: update checkout
|
|
tx.commit();
|
|
// TODO: update working copy
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_debug(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
if let Some(resolve_matches) = sub_matches.subcommand_matches("resolverev") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit = resolve_revision_arg(ui, mut_repo, resolve_matches)?;
|
|
writeln!(ui, "{}", commit.id().hex());
|
|
} else if let Some(_wc_matches) = sub_matches.subcommand_matches("workingcopy") {
|
|
let repo = get_repo(ui, &matches)?;
|
|
let wc = repo.working_copy_locked();
|
|
writeln!(ui, "Current commit: {:?}", wc.current_commit_id());
|
|
writeln!(ui, "Current tree: {:?}", wc.current_tree_id());
|
|
for (file, state) in wc.file_states().iter() {
|
|
writeln!(
|
|
ui,
|
|
"{:?} {:13?} {:10?} {:?}",
|
|
state.file_type, state.size, state.mtime.0, file
|
|
);
|
|
}
|
|
} else if let Some(_wc_matches) = sub_matches.subcommand_matches("writeworkingcopy") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let wc = owned_wc.lock().unwrap();
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let old_commit_id = wc.current_commit_id();
|
|
let new_commit_id = wc.commit(ui.settings(), mut_repo).id().clone();
|
|
writeln!(ui, "old commit {:?}", old_commit_id);
|
|
writeln!(ui, "new commit {:?}", new_commit_id);
|
|
} else if let Some(template_matches) = sub_matches.subcommand_matches("template") {
|
|
let parse = TemplateParser::parse(
|
|
crate::template_parser::Rule::template,
|
|
template_matches.value_of("template").unwrap(),
|
|
);
|
|
writeln!(ui, "{:?}", parse);
|
|
} else if let Some(_reindex_matches) = sub_matches.subcommand_matches("index") {
|
|
let repo = get_repo(ui, &matches)?;
|
|
let index = repo.index();
|
|
let stats = index.as_composite().stats();
|
|
writeln!(ui, "Number of commits: {}", stats.num_commits);
|
|
writeln!(ui, "Number of merges: {}", stats.num_merges);
|
|
writeln!(ui, "Max generation number: {}", stats.max_generation_number);
|
|
writeln!(ui, "Number of heads: {}", stats.num_heads);
|
|
writeln!(ui, "Stats per level:");
|
|
for (i, level) in stats.levels.iter().enumerate() {
|
|
writeln!(ui, " Level {}:", i);
|
|
writeln!(ui, " Number of commits: {}", level.num_commits);
|
|
writeln!(ui, " Name: {}", level.name.as_ref().unwrap());
|
|
}
|
|
} else if let Some(_reindex_matches) = sub_matches.subcommand_matches("reindex") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let index = mut_repo.reindex();
|
|
writeln!(
|
|
ui,
|
|
"Finished indexing {:?} commits.",
|
|
index.as_composite().num_commits()
|
|
);
|
|
} else {
|
|
panic!("unhandled command: {:#?}", matches);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_bench(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut criterion = Criterion::default();
|
|
if let Some(command_matches) = sub_matches.subcommand_matches("commonancestors") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let commit1 =
|
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision1").unwrap())?;
|
|
let commit2 =
|
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision2").unwrap())?;
|
|
let routine = || common_ancestor(vec![&commit1], vec![&commit2]);
|
|
writeln!(ui, "Result: {:?}", routine());
|
|
criterion.bench_function("commonancestors", |bencher: &mut criterion::Bencher| {
|
|
bencher.iter(routine);
|
|
});
|
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("isancestor") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let ancestor_commit =
|
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("ancestor").unwrap())?;
|
|
let descendant_commit = resolve_single_rev(
|
|
ui,
|
|
mut_repo,
|
|
command_matches.value_of("descendant").unwrap(),
|
|
)?;
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
let routine = || index.is_ancestor(ancestor_commit.id(), descendant_commit.id());
|
|
writeln!(ui, "Result: {:?}", routine());
|
|
criterion.bench_function("isancestor", |bencher: &mut criterion::Bencher| {
|
|
bencher.iter(routine);
|
|
});
|
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("walkrevs") {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let mut_repo = Arc::get_mut(&mut repo).unwrap();
|
|
let unwanted_commit =
|
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("unwanted").unwrap())?;
|
|
let wanted_commit =
|
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("wanted").unwrap())?;
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
let routine = || {
|
|
index
|
|
.walk_revs(
|
|
&[wanted_commit.id().clone()],
|
|
&[unwanted_commit.id().clone()],
|
|
)
|
|
.count()
|
|
};
|
|
writeln!(ui, "Result: {:?}", routine());
|
|
criterion.bench_function("walkrevs", |bencher: &mut criterion::Bencher| {
|
|
bencher.iter(routine);
|
|
});
|
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("resolveprefix") {
|
|
let repo = get_repo(ui, &matches)?;
|
|
let prefix = HexPrefix::new(command_matches.value_of("prefix").unwrap().to_string());
|
|
let index = repo.index();
|
|
let index = index.as_composite();
|
|
let routine = || index.resolve_prefix(&prefix);
|
|
writeln!(ui, "Result: {:?}", routine());
|
|
criterion.bench_function("resolveprefix", |bencher: &mut criterion::Bencher| {
|
|
bencher.iter(routine);
|
|
});
|
|
} else {
|
|
panic!("unhandled command: {:#?}", matches);
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn format_timestamp(timestamp: &Timestamp) -> String {
|
|
let utc = Utc
|
|
.timestamp(
|
|
timestamp.timestamp.0 as i64 / 1000,
|
|
(timestamp.timestamp.0 % 1000) as u32 * 1000000,
|
|
)
|
|
.with_timezone(&FixedOffset::east(timestamp.tz_offset * 60));
|
|
utc.format("%Y-%m-%d %H:%M:%S.%3f %:z").to_string()
|
|
}
|
|
|
|
fn cmd_op_log(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
_op_matches: &ArgMatches,
|
|
_cmd_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let repo = get_repo(ui, &matches)?;
|
|
let view = repo.view();
|
|
let head_op = view.base_op_head();
|
|
let mut styler = ui.styler();
|
|
let mut styler = styler.as_mut();
|
|
struct OpTemplate;
|
|
impl Template<Operation> for OpTemplate {
|
|
fn format(&self, op: &Operation, styler: &mut dyn Styler) {
|
|
// TODO: why can't this label be applied outside of the template?
|
|
styler.add_label("op-log".to_string());
|
|
// TODO: Make this templated
|
|
styler.add_label("id".to_string());
|
|
// TODO: support lookup by op-id prefix, so we don't need to print the full hash
|
|
// here
|
|
styler.write_str(&op.id().hex());
|
|
styler.remove_label();
|
|
styler.write_str(" ");
|
|
let metadata = &op.store_operation().metadata;
|
|
styler.add_label("user".to_string());
|
|
styler.write_str(&format!("{}@{}", metadata.username, metadata.hostname));
|
|
styler.remove_label();
|
|
styler.write_str(" ");
|
|
styler.add_label("time".to_string());
|
|
styler.write_str(&format!(
|
|
"{} - {}",
|
|
format_timestamp(&metadata.start_time),
|
|
format_timestamp(&metadata.end_time)
|
|
));
|
|
styler.remove_label();
|
|
styler.write_str("\n");
|
|
styler.add_label("description".to_string());
|
|
styler.write_str(&metadata.description);
|
|
styler.remove_label();
|
|
|
|
styler.remove_label();
|
|
}
|
|
}
|
|
let template = OpTemplate;
|
|
|
|
let mut graph = AsciiGraphDrawer::new(&mut styler);
|
|
for op in topo_order_reverse(
|
|
vec![head_op],
|
|
Box::new(|op: &Operation| op.id().clone()),
|
|
Box::new(|op: &Operation| op.parents()),
|
|
) {
|
|
let mut edges = vec![];
|
|
for parent in op.parents() {
|
|
edges.push(Edge::direct(parent.id().clone()));
|
|
}
|
|
let mut buffer = vec![];
|
|
// TODO: only use color if requested
|
|
{
|
|
let writer = Box::new(&mut buffer);
|
|
let mut styler = ColorStyler::new(writer, ui.settings());
|
|
template.format(&op, &mut styler);
|
|
}
|
|
if !buffer.ends_with(b"\n") {
|
|
buffer.push(b'\n');
|
|
}
|
|
graph.add_node(op.id(), &edges, b"o", &buffer);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_op_undo(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
_op_matches: &ArgMatches,
|
|
_cmd_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let bad_op = resolve_single_op(&repo, _cmd_matches.value_of("operation").unwrap())?;
|
|
let parent_ops = bad_op.parents();
|
|
if parent_ops.len() > 1 {
|
|
return Err(CommandError::UserError(
|
|
"Cannot undo a merge operation".to_string(),
|
|
));
|
|
}
|
|
if parent_ops.is_empty() {
|
|
return Err(CommandError::UserError(
|
|
"Cannot undo repo initialization".to_string(),
|
|
));
|
|
}
|
|
|
|
let fixed_view = {
|
|
let view = repo.view();
|
|
let parent_view = parent_ops[0].view();
|
|
let bad_view = bad_op.view();
|
|
let current_view = view.base_op_head().view();
|
|
merge_views(
|
|
repo.store(),
|
|
current_view.store_view(),
|
|
bad_view.store_view(),
|
|
parent_view.store_view(),
|
|
)
|
|
};
|
|
|
|
let mut tx = repo.start_transaction(&format!("undo operation {}", bad_op.id().hex()));
|
|
tx.set_view(fixed_view);
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
fn cmd_op_restore(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
_op_matches: &ArgMatches,
|
|
_cmd_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
let mut repo = get_repo(ui, &matches)?;
|
|
let owned_wc = repo.working_copy().clone();
|
|
let target_op = resolve_single_op(&repo, _cmd_matches.value_of("operation").unwrap())?;
|
|
let mut tx = repo.start_transaction(&format!("restore to operation {}", target_op.id().hex()));
|
|
tx.set_view(target_op.view().take_store_view());
|
|
tx.commit();
|
|
update_working_copy(
|
|
ui,
|
|
Arc::get_mut(&mut repo).unwrap(),
|
|
&owned_wc.lock().unwrap(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_operation(
|
|
ui: &mut Ui,
|
|
matches: &ArgMatches,
|
|
sub_matches: &ArgMatches,
|
|
) -> Result<(), CommandError> {
|
|
if let Some(command_matches) = sub_matches.subcommand_matches("log") {
|
|
cmd_op_log(ui, matches, sub_matches, command_matches)?;
|
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("undo") {
|
|
cmd_op_undo(ui, matches, sub_matches, command_matches)?;
|
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("restore") {
|
|
cmd_op_restore(ui, matches, sub_matches, command_matches)?;
|
|
} else {
|
|
panic!("unhandled command: {:#?}", matches);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn dispatch<I, T>(mut ui: Ui, args: I) -> i32
|
|
where
|
|
I: IntoIterator<Item = T>,
|
|
T: Into<OsString> + Clone,
|
|
{
|
|
let matches = get_app().get_matches_from(args);
|
|
if matches.subcommand_name().is_none() {
|
|
let mut help_text_buf = Vec::new();
|
|
get_app().write_long_help(&mut help_text_buf).unwrap();
|
|
ui.write(String::from_utf8(help_text_buf).unwrap().as_str());
|
|
ui.write("\n");
|
|
return 1;
|
|
}
|
|
let result = if let Some(sub_matches) = matches.subcommand_matches("init") {
|
|
cmd_init(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("checkout") {
|
|
cmd_checkout(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("files") {
|
|
cmd_files(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("diff") {
|
|
cmd_diff(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("status") {
|
|
cmd_status(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("log") {
|
|
cmd_log(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("obslog") {
|
|
cmd_obslog(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("describe") {
|
|
cmd_describe(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("close") {
|
|
cmd_close(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("open") {
|
|
cmd_open(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("duplicate") {
|
|
cmd_duplicate(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("prune") {
|
|
cmd_prune(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("new") {
|
|
cmd_new(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("squash") {
|
|
cmd_squash(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("discard") {
|
|
cmd_discard(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("restore") {
|
|
cmd_restore(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("merge") {
|
|
cmd_merge(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("rebase") {
|
|
cmd_rebase(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("backout") {
|
|
cmd_backout(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("evolve") {
|
|
cmd_evolve(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("operation") {
|
|
cmd_operation(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("bench") {
|
|
cmd_bench(&mut ui, &matches, &sub_matches)
|
|
} else if let Some(sub_matches) = matches.subcommand_matches("debug") {
|
|
cmd_debug(&mut ui, &matches, &sub_matches)
|
|
} else {
|
|
panic!("unhandled command: {:#?}", matches);
|
|
};
|
|
match result {
|
|
Ok(()) => 0,
|
|
Err(CommandError::UserError(message)) => {
|
|
ui.write_error(format!("Error: {}\n", message).as_str());
|
|
1
|
|
}
|
|
Err(CommandError::InternalError(message)) => {
|
|
ui.write_error(format!("Internal error: {}\n", message).as_str());
|
|
255
|
|
}
|
|
}
|
|
}
|