userland/git: git-compatible CLI on v2's gix code (T10)
Local workflow, smart-HTTP clone/fetch/push and public HTTPS clone/fetch match native git in stdout and exit codes. Only WASI imports (wasi:http, exit-with-code).
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
//! Linear rebase sequencer. Every entry point is called under the common .git lock.
|
||||
//!
|
||||
//! Own persistent format (NOT Git's sequencer format): rebase-merge/sausage.json,
|
||||
//! version 1. Full hex OIDs; onto/orig_head/orig_tip/branch are immutable; todo is
|
||||
//! oldest-first, done lists consumed original commits (including skipped/empty),
|
||||
//! current is the conflicted original commit. tip is the unpublished replay tip.
|
||||
//! work_tree records the last materialized tree; orig_index is base64 of the exact
|
||||
//! original index. phase checkpoints precede worktree/ref writes. Atomic JSON rename
|
||||
//! makes each pick restartable; refs stay original until Finish. New fields must
|
||||
//! default; unknown versions refuse. Real git is neither used nor expected to read it.
|
||||
use crate::Result;
|
||||
use crate::{branches, checkout, merge, repo::error, staging};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use gix::bstr::ByteSlice;
|
||||
use std::{collections::BTreeSet, fs};
|
||||
|
||||
/// Replay a local branch's linear commits onto another revision.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct RebaseParams {
|
||||
/// Plain name under /repos (a-z0-9._-, no '..'), or absolute path under /plugins/ or /repos/ (no empty, '.' or '..' segments or trailing slash).
|
||||
pub repo: String,
|
||||
/// New base; defaults to the selected branch's configured upstream.
|
||||
pub onto: Option<String>,
|
||||
/// Short or full local branch name; defaults to current branch (not detached HEAD).
|
||||
pub branch: Option<String>,
|
||||
}
|
||||
/// Rebase progress. HEAD and the branch ref remain original until completion.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct RebaseResult {
|
||||
/// HEAD after this call (original while pending).
|
||||
pub head: crate::HeadInfo,
|
||||
/// Unpublished replay tip while pending; final branch tip otherwise.
|
||||
pub tip: String,
|
||||
/// True when continue, skip or abort is needed.
|
||||
pub pending: bool,
|
||||
/// True when the selected branch already contains the requested base.
|
||||
pub already_up_to_date: bool,
|
||||
/// True when no replay was required and the branch advanced to onto.
|
||||
pub fast_forward: bool,
|
||||
/// Sorted unresolved paths in stages 1/2/3.
|
||||
pub conflicts: Vec<String>,
|
||||
/// Checkout limitations, including preserved abort obstructions.
|
||||
pub warnings: Vec<String>,
|
||||
/// Commit created from a staged conflict resolution during this call.
|
||||
pub resolved: Option<String>,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
enum Phase {
|
||||
Replay,
|
||||
CheckoutConflict,
|
||||
Conflict,
|
||||
Reset,
|
||||
Finish,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct State {
|
||||
version: u32,
|
||||
onto: String,
|
||||
orig_head: String,
|
||||
orig_branch: Option<String>,
|
||||
branch: String,
|
||||
orig_tip: String,
|
||||
tip: String,
|
||||
todo: Vec<String>,
|
||||
done: Vec<String>,
|
||||
current: Option<String>,
|
||||
work_tree: String,
|
||||
orig_index: Option<String>,
|
||||
phase: Phase,
|
||||
already: bool,
|
||||
fast_forward: bool,
|
||||
}
|
||||
fn oid(s: &str) -> Result<gix::ObjectId> {
|
||||
gix::ObjectId::from_hex(s.as_bytes()).map_err(|e| error("rebase state OID", e))
|
||||
}
|
||||
fn tree(repo: &gix::Repository, id: gix::ObjectId) -> Result<gix::ObjectId> {
|
||||
Ok(repo
|
||||
.find_commit(id)
|
||||
.map_err(|e| error("rebase commit", e))?
|
||||
.tree_id()
|
||||
.map_err(|e| error("rebase tree", e))?
|
||||
.detach())
|
||||
}
|
||||
fn state_path(repo: &gix::Repository) -> std::path::PathBuf {
|
||||
repo.git_dir().join("rebase-merge/sausage.json")
|
||||
}
|
||||
fn save(repo: &gix::Repository, s: &State) -> Result<()> {
|
||||
let bytes = serde_json::to_vec(s).map_err(|e| error("rebase state", e))?;
|
||||
let tmp = repo.git_dir().join("rebase-merge/sausage.json.tmp");
|
||||
fs::write(&tmp, bytes).map_err(|e| error("rebase state write", e))?;
|
||||
fs::rename(tmp, state_path(repo)).map_err(|e| error("rebase state rename", e))
|
||||
}
|
||||
fn load(repo: &gix::Repository) -> Result<State> {
|
||||
let bytes = fs::read(state_path(repo))
|
||||
.map_err(|e| error("rebase: no readable Sausage rebase state", e))?;
|
||||
let s: State = serde_json::from_slice(&bytes).map_err(|e| error("rebase state", e))?;
|
||||
if s.version != 1 {
|
||||
return Err(error("rebase state", "unsupported version"));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
fn clear(repo: &gix::Repository) -> Result<()> {
|
||||
fs::remove_file(state_path(repo)).map_err(|e| error("rebase cleanup", e))?;
|
||||
let tmp = repo.git_dir().join("rebase-merge/sausage.json.tmp");
|
||||
if tmp.exists() {
|
||||
fs::remove_file(tmp).map_err(|e| error("rebase cleanup", e))?;
|
||||
}
|
||||
// Never recursively remove someone else's sequencer files.
|
||||
fs::remove_dir(repo.git_dir().join("rebase-merge")).map_err(|e| error("rebase cleanup", e))
|
||||
}
|
||||
fn conflicts(repo: &gix::Repository) -> Result<Vec<String>> {
|
||||
let index = repo
|
||||
.index_or_empty()
|
||||
.map_err(|e| error("rebase index", e))?;
|
||||
Ok(index
|
||||
.entries()
|
||||
.iter()
|
||||
.filter(|e| e.stage_raw() != 0)
|
||||
.map(|e| e.path(&index).to_str_lossy().into_owned())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
fn result(
|
||||
repo: &gix::Repository,
|
||||
s: &State,
|
||||
pending: bool,
|
||||
warnings: Vec<String>,
|
||||
) -> Result<RebaseResult> {
|
||||
Ok(RebaseResult {
|
||||
head: crate::head_info(repo)?,
|
||||
tip: s.tip.clone(),
|
||||
pending,
|
||||
already_up_to_date: s.already,
|
||||
fast_forward: s.fast_forward,
|
||||
conflicts: if pending { conflicts(repo)? } else { vec![] },
|
||||
warnings,
|
||||
resolved: None,
|
||||
})
|
||||
}
|
||||
fn index_lock(repo: &gix::Repository) -> Result<()> {
|
||||
if repo.git_dir().join("index.lock").exists() {
|
||||
return Err(error("rebase", "index.lock already exists"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn verify_refs(repo: &gix::Repository, s: &State) -> Result<()> {
|
||||
let head = crate::head_info(repo)?;
|
||||
if head.id.as_deref() != Some(&s.orig_head)
|
||||
|| head.branch != s.orig_branch
|
||||
|| branches::resolve(repo, &s.branch)? != oid(&s.orig_tip)?
|
||||
{
|
||||
return Err(error(
|
||||
"rebase",
|
||||
"HEAD or branch changed since rebase began; refusing to overwrite refs",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn upstream(repo: &gix::Repository, branch: &str) -> Result<String> {
|
||||
// gix's remote-tracking lookup doesn't map local (remote=".") upstreams.
|
||||
let short = branch
|
||||
.strip_prefix("refs/heads/")
|
||||
.ok_or_else(|| error("rebase upstream", "expected local branch"))?;
|
||||
let config = repo.config_snapshot();
|
||||
if config
|
||||
.string(format!("branch.{short}.remote").as_str())
|
||||
.is_some_and(|s| s.as_bstr() == b".".as_bstr())
|
||||
{
|
||||
return config
|
||||
.string(format!("branch.{short}.merge").as_str())
|
||||
.map(|s| s.to_str_lossy().into_owned())
|
||||
.ok_or_else(|| error("rebase", "no upstream merge branch configured; supply onto"));
|
||||
}
|
||||
repo.find_reference(branch)
|
||||
.map_err(|e| error("rebase branch", e))?
|
||||
.remote_tracking_ref_name(gix::remote::Direction::Fetch)
|
||||
.transpose()
|
||||
.map_err(|e| error("rebase upstream", e))?
|
||||
.map(|n| n.as_bstr().to_str_lossy().into_owned())
|
||||
.ok_or_else(|| {
|
||||
error(
|
||||
"rebase",
|
||||
"no upstream configured; supply onto or configure branch upstream",
|
||||
)
|
||||
})
|
||||
}
|
||||
pub fn start(repo: &gix::Repository, p: RebaseParams) -> Result<RebaseResult> {
|
||||
let local = merge::merge_repository(repo)?;
|
||||
let repo = &local;
|
||||
merge::ensure_idle(repo)?;
|
||||
merge::clean(repo)?;
|
||||
let head = crate::head_info(repo)?;
|
||||
let orig_head = head
|
||||
.id
|
||||
.ok_or_else(|| error("rebase", "unborn HEAD is unsupported"))?;
|
||||
let branch = p
|
||||
.branch
|
||||
.map(|b| {
|
||||
if b.starts_with("refs/heads/") {
|
||||
b
|
||||
} else {
|
||||
format!("refs/heads/{b}")
|
||||
}
|
||||
})
|
||||
.or_else(|| head.branch.clone())
|
||||
.ok_or_else(|| error("rebase", "detached HEAD; supply a local branch"))?;
|
||||
let _: gix::refs::FullName = branch
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| error("rebase branch", e))?;
|
||||
if !branch.starts_with("refs/heads/") {
|
||||
return Err(error("rebase", "expected local branch"));
|
||||
}
|
||||
let orig = branches::resolve(repo, &branch)?;
|
||||
let onto = branches::resolve(
|
||||
repo,
|
||||
&match p.onto {
|
||||
Some(r) => r,
|
||||
None => upstream(repo, &branch)?,
|
||||
},
|
||||
)?;
|
||||
let base = repo
|
||||
.merge_base(orig, onto)
|
||||
.map_err(|e| error("rebase merge-base", e))?
|
||||
.ok_or_else(|| error("rebase", "unrelated histories are unsupported"))?
|
||||
.detach();
|
||||
let mut cursor = orig;
|
||||
let mut todo = Vec::new();
|
||||
while cursor != base {
|
||||
let c = repo
|
||||
.find_commit(cursor)
|
||||
.map_err(|e| error("rebase range", e))?;
|
||||
let parents: Vec<_> = c.parent_ids().map(|id| id.detach()).collect();
|
||||
if parents.len() != 1 {
|
||||
return Err(error(
|
||||
"rebase",
|
||||
"linear history only: merge commit in replay range (no rebase-merges)",
|
||||
));
|
||||
}
|
||||
todo.push(cursor.to_string());
|
||||
cursor = parents[0];
|
||||
}
|
||||
todo.reverse();
|
||||
let already = base == onto;
|
||||
if already && head.branch.as_deref() == Some(&branch) {
|
||||
return Ok(RebaseResult {
|
||||
head: crate::head_info(repo)?,
|
||||
tip: orig.to_string(),
|
||||
pending: false,
|
||||
already_up_to_date: true,
|
||||
fast_forward: false,
|
||||
conflicts: vec![],
|
||||
warnings: vec![],
|
||||
resolved: None,
|
||||
});
|
||||
}
|
||||
if already {
|
||||
todo.clear();
|
||||
}
|
||||
if !todo.is_empty() {
|
||||
staging::signature(None, None)?;
|
||||
}
|
||||
// Preflight reflog configuration before materializing anything.
|
||||
repo.committer()
|
||||
.transpose()
|
||||
.map_err(|e| error("rebase identity", e))?;
|
||||
let orig_index = match fs::read(repo.git_dir().join("index")) {
|
||||
Ok(b) => Some(STANDARD.encode(b)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(e) => return Err(error("rebase snapshot index", e)),
|
||||
};
|
||||
let s = State {
|
||||
version: 1,
|
||||
onto: onto.to_string(),
|
||||
orig_head: orig_head.clone(),
|
||||
orig_branch: head.branch,
|
||||
branch,
|
||||
orig_tip: orig.to_string(),
|
||||
tip: if already { orig } else { onto }.to_string(),
|
||||
todo,
|
||||
done: vec![],
|
||||
current: None,
|
||||
work_tree: tree(repo, oid(&orig_head)?)?.to_string(),
|
||||
orig_index,
|
||||
phase: Phase::Replay,
|
||||
already,
|
||||
fast_forward: !already && base == orig,
|
||||
};
|
||||
fs::create_dir(repo.git_dir().join("rebase-merge"))
|
||||
.map_err(|e| error("rebase state directory", e))?;
|
||||
save(repo, &s)?;
|
||||
run(repo, s, vec![])
|
||||
}
|
||||
fn pick(
|
||||
repo: &gix::Repository,
|
||||
original: &str,
|
||||
tip: &str,
|
||||
new_tree: gix::ObjectId,
|
||||
) -> Result<String> {
|
||||
if new_tree == tree(repo, oid(tip)?)? {
|
||||
return Ok(tip.to_owned());
|
||||
}
|
||||
let original = repo
|
||||
.find_commit(oid(original)?)
|
||||
.map_err(|e| error("rebase original", e))?;
|
||||
let decoded = original.decode().map_err(|e| error("rebase original", e))?;
|
||||
let author = decoded
|
||||
.author()
|
||||
.map_err(|e| error("rebase author", e))?
|
||||
.to_owned()
|
||||
.map_err(|e| error("rebase author", e))?;
|
||||
let object = gix::objs::Commit {
|
||||
tree: new_tree,
|
||||
parents: vec![oid(tip)?].into_iter().collect(),
|
||||
author,
|
||||
committer: staging::signature(None, None)?,
|
||||
encoding: decoded.encoding.map(ToOwned::to_owned),
|
||||
message: decoded.message.to_owned(),
|
||||
extra_headers: vec![],
|
||||
};
|
||||
Ok(repo
|
||||
.write_object(&object)
|
||||
.map_err(|e| error("rebase commit write", e))?
|
||||
.to_string())
|
||||
}
|
||||
fn consume(s: &mut State) {
|
||||
if !s.todo.is_empty() {
|
||||
s.done.push(s.todo.remove(0));
|
||||
}
|
||||
s.current = None;
|
||||
}
|
||||
fn run(repo: &gix::Repository, mut s: State, mut warnings: Vec<String>) -> Result<RebaseResult> {
|
||||
verify_refs(repo, &s)?;
|
||||
while let Some(current) = s.todo.first().cloned() {
|
||||
let commit = repo
|
||||
.find_commit(oid(¤t)?)
|
||||
.map_err(|e| error("rebase pick", e))?;
|
||||
let parent = commit
|
||||
.parent_ids()
|
||||
.next()
|
||||
.ok_or_else(|| error("rebase pick", "root commit in range"))?
|
||||
.detach();
|
||||
let labels = gix::merge::blob::builtin_driver::text::Labels {
|
||||
ancestor: None,
|
||||
current: Some("ours".into()),
|
||||
other: Some("theirs".into()),
|
||||
};
|
||||
let mut outcome = repo
|
||||
.merge_trees(
|
||||
tree(repo, parent)?,
|
||||
tree(repo, oid(&s.tip)?)?,
|
||||
tree(repo, oid(¤t)?)?,
|
||||
labels,
|
||||
repo.tree_merge_options()
|
||||
.map_err(|e| error("rebase options", e))?,
|
||||
)
|
||||
.map_err(|e| error("rebase merge trees", e))?;
|
||||
let unresolved = outcome.has_unresolved_conflicts(Default::default());
|
||||
let merged = outcome
|
||||
.tree
|
||||
.write()
|
||||
.map_err(|e| error("rebase tree write", e))?
|
||||
.detach();
|
||||
let mut index = gix::index::State::from_tree(&merged, &repo.objects, Default::default())
|
||||
.map_err(|e| error("rebase index", e))?;
|
||||
outcome.index_changed_after_applying_conflicts(
|
||||
&mut index,
|
||||
Default::default(),
|
||||
gix::merge::tree::apply_index_entries::RemovalMode::Prune,
|
||||
);
|
||||
index.sort_entries();
|
||||
let has_conflicts = index.entries().iter().any(|e| e.stage_raw() != 0);
|
||||
if unresolved && !has_conflicts {
|
||||
return Err(error(
|
||||
"rebase",
|
||||
"gix conflict without index stages; refusing unsafe replay",
|
||||
));
|
||||
}
|
||||
if has_conflicts {
|
||||
s.current = Some(current);
|
||||
s.phase = Phase::CheckoutConflict;
|
||||
save(repo, &s)?;
|
||||
warnings.extend(checkout::write_tree_to_worktree(
|
||||
repo,
|
||||
Some(oid(&s.work_tree)?),
|
||||
merged,
|
||||
false,
|
||||
)?);
|
||||
staging::publish(repo, index)?;
|
||||
s.work_tree = merged.to_string();
|
||||
s.phase = Phase::Conflict;
|
||||
save(repo, &s)?;
|
||||
return result(repo, &s, true, warnings);
|
||||
}
|
||||
s.tip = pick(repo, ¤t, &s.tip, merged)?;
|
||||
consume(&mut s);
|
||||
save(repo, &s)?;
|
||||
}
|
||||
s.phase = Phase::Finish;
|
||||
save(repo, &s)?;
|
||||
finish(repo, s, warnings)
|
||||
}
|
||||
fn finish(repo: &gix::Repository, s: State, mut warnings: Vec<String>) -> Result<RebaseResult> {
|
||||
verify_refs(repo, &s)?;
|
||||
let target = tree(repo, oid(&s.tip)?)?;
|
||||
// A process may exit after checkout publishes its index but before the ref CAS.
|
||||
// In that case the worktree is already final; do not compare it to the old tree.
|
||||
if resolved_tree(repo)? != target {
|
||||
warnings.extend(checkout::write_tree_to_worktree(
|
||||
repo,
|
||||
Some(oid(&s.work_tree)?),
|
||||
target,
|
||||
false,
|
||||
)?);
|
||||
}
|
||||
use gix::refs::{
|
||||
Target,
|
||||
transaction::{Change, PreviousValue, RefEdit},
|
||||
};
|
||||
let old_head = match &s.orig_branch {
|
||||
Some(b) => Target::Symbolic(b.as_str().try_into().map_err(|e| error("rebase HEAD", e))?),
|
||||
None => Target::Object(oid(&s.orig_head)?),
|
||||
};
|
||||
warnings.extend(branches::edit_refs(
|
||||
repo,
|
||||
vec![
|
||||
RefEdit {
|
||||
name: s
|
||||
.branch
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| error("rebase branch", e))?,
|
||||
deref: false,
|
||||
change: Change::Update {
|
||||
expected: PreviousValue::MustExistAndMatch(Target::Object(oid(&s.orig_tip)?)),
|
||||
new: Target::Object(oid(&s.tip)?),
|
||||
log: Default::default(),
|
||||
},
|
||||
},
|
||||
RefEdit {
|
||||
name: "HEAD".try_into().map_err(|e| error("rebase HEAD", e))?,
|
||||
deref: false,
|
||||
change: Change::Update {
|
||||
expected: PreviousValue::MustExistAndMatch(old_head),
|
||||
new: Target::Symbolic(
|
||||
s.branch
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| error("rebase branch", e))?,
|
||||
),
|
||||
log: Default::default(),
|
||||
},
|
||||
},
|
||||
],
|
||||
)?);
|
||||
clear(repo)?;
|
||||
result(repo, &s, false, warnings)
|
||||
}
|
||||
/// Check only unstaged changes: HEAD intentionally still names the original branch tip.
|
||||
fn resolved_tree(repo: &gix::Repository) -> Result<gix::ObjectId> {
|
||||
let index = repo
|
||||
.index_or_empty()
|
||||
.map_err(|e| error("rebase index", e))?;
|
||||
let tree = staging::tree_from_index(repo, &index)?;
|
||||
let status = crate::changes::status(repo)?;
|
||||
if !status.unstaged.is_empty() {
|
||||
return Err(error(
|
||||
"rebase continue",
|
||||
"unstaged changes; stage the resolved paths first",
|
||||
));
|
||||
}
|
||||
Ok(tree)
|
||||
}
|
||||
pub fn resume(repo: &gix::Repository, skip: bool) -> Result<RebaseResult> {
|
||||
index_lock(repo)?;
|
||||
let local = merge::merge_repository(repo)?;
|
||||
let repo = &local;
|
||||
let mut s = load(repo)?;
|
||||
// Ref transaction completed but cleanup was interrupted: do not replay twice.
|
||||
let head = crate::head_info(repo)?;
|
||||
if s.phase == Phase::Finish
|
||||
&& head.branch.as_deref() == Some(&s.branch)
|
||||
&& head.id.as_deref() == Some(&s.tip)
|
||||
&& branches::resolve(repo, &s.branch)? == oid(&s.tip)?
|
||||
{
|
||||
clear(repo)?;
|
||||
return result(repo, &s, false, vec![]);
|
||||
}
|
||||
verify_refs(repo, &s)?;
|
||||
if s.phase == Phase::CheckoutConflict {
|
||||
return Err(error(
|
||||
"rebase",
|
||||
"interrupted conflict publication; use rebase_abort (resolution index may be incomplete)",
|
||||
));
|
||||
}
|
||||
if skip && s.phase != Phase::Conflict {
|
||||
return Err(error("rebase skip", "no conflicted pick to skip"));
|
||||
}
|
||||
let mut warnings = vec![];
|
||||
let mut resolved = None;
|
||||
if s.phase == Phase::Conflict {
|
||||
if skip {
|
||||
consume(&mut s);
|
||||
s.phase = Phase::Reset;
|
||||
save(repo, &s)?;
|
||||
} else {
|
||||
let merged = resolved_tree(repo)?;
|
||||
let current = s
|
||||
.current
|
||||
.as_deref()
|
||||
.ok_or_else(|| error("rebase state", "missing current pick"))?;
|
||||
s.tip = pick(repo, current, &s.tip, merged)?;
|
||||
resolved = Some(s.tip.clone());
|
||||
s.work_tree = merged.to_string();
|
||||
consume(&mut s);
|
||||
s.phase = Phase::Replay;
|
||||
save(repo, &s)?;
|
||||
}
|
||||
}
|
||||
if s.phase == Phase::Reset {
|
||||
let target = tree(repo, oid(&s.tip)?)?;
|
||||
warnings.extend(checkout::write_tree_to_worktree(
|
||||
repo,
|
||||
Some(oid(&s.work_tree)?),
|
||||
target,
|
||||
true,
|
||||
)?);
|
||||
s.work_tree = target.to_string();
|
||||
s.phase = Phase::Replay;
|
||||
save(repo, &s)?;
|
||||
}
|
||||
if s.phase == Phase::Finish {
|
||||
return finish(repo, s, warnings);
|
||||
}
|
||||
if resolved_tree(repo)? != oid(&s.work_tree)? {
|
||||
return Err(error(
|
||||
"rebase",
|
||||
"index changed during interrupted replay; abort or restore the index before continuing",
|
||||
));
|
||||
}
|
||||
let mut result = run(repo, s, warnings)?;
|
||||
result.resolved = resolved;
|
||||
Ok(result)
|
||||
}
|
||||
pub fn abort(repo: &gix::Repository) -> Result<RebaseResult> {
|
||||
index_lock(repo)?;
|
||||
let s = load(repo)?;
|
||||
verify_refs(repo, &s)?;
|
||||
let warnings = checkout::abort_to_tree(repo, tree(repo, oid(&s.orig_head)?)?)?;
|
||||
if let Some(bytes) = &s.orig_index {
|
||||
use std::io::Write;
|
||||
let bytes = STANDARD
|
||||
.decode(bytes)
|
||||
.map_err(|e| error("rebase original index", e))?;
|
||||
let lock = repo.git_dir().join("index.lock");
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock)
|
||||
.map_err(|e| error("rebase index lock", e))?;
|
||||
let write = file.write_all(&bytes).and_then(|_| file.flush());
|
||||
drop(file);
|
||||
if let Err(e) = write {
|
||||
let _ = fs::remove_file(&lock);
|
||||
return Err(error("rebase index restore", e));
|
||||
}
|
||||
fs::rename(&lock, repo.git_dir().join("index"))
|
||||
.map_err(|e| error("rebase index restore", e))?;
|
||||
} else {
|
||||
match fs::remove_file(repo.git_dir().join("index")) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(error("rebase index restore", e)),
|
||||
}
|
||||
}
|
||||
clear(repo)?;
|
||||
let mut r = result(repo, &s, false, warnings)?;
|
||||
r.tip = s.orig_tip;
|
||||
Ok(r)
|
||||
}
|
||||
Reference in New Issue
Block a user