402 lines · 12.3 KB · Rust
RawDownload
// Copyright 2025 The Jujutsu Authors//// 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. use std::path::Path;use std::path::PathBuf; use gix::date::parse::TimeBuf; pub const GIT_USER: &str = "Someone";pub const GIT_EMAIL: &str = "someone@example.org"; fn git_config() -> Vec<bstr::BString> {    vec![        format!("user.name = {GIT_USER}").into(),        format!("user.email = {GIT_EMAIL}").into(),        "init.defaultBranch = master".into(),    ]} fn open_options() -> gix::open::Options {    gix::open::Options::isolated()        .config_overrides(git_config())        .strict_config(true)} pub fn open(directory: impl Into<PathBuf>) -> gix::Repository {    gix::open_opts(directory, open_options()).unwrap()} pub fn init(directory: impl AsRef<Path>) -> gix::Repository {    gix::ThreadSafeRepository::init_opts(        directory,        gix::create::Kind::WithWorktree,        gix::create::Options::default(),        open_options(),    )    .unwrap()    .to_thread_local()} pub fn init_bare(directory: impl AsRef<Path>) -> gix::Repository {    gix::ThreadSafeRepository::init_opts(        directory,        gix::create::Kind::Bare,        gix::create::Options::default(),        open_options(),    )    .unwrap()    .to_thread_local()} pub fn clone(dest_path: &Path, repo_url: &str, remote_name: Option<&str>) -> gix::Repository {    let remote_name = remote_name.unwrap_or("origin");    // gitoxide doesn't write the remote HEAD as a symbolic link, which prevents    // `jj` from getting it.    //    // This, plus the fact that the code to clone a repo in gitoxide is non-trivial,    // makes it appealing to just spawn a git subprocess    let output = std::process::Command::new("git")        .args(["clone", repo_url, "--origin", remote_name])        .arg(dest_path)        .output()        .unwrap();    assert!(        output.status.success(),        "git cloning failed with {}:\n{}\n----- stderr -----\n{}",        output.status,        bstr::BString::from(output.stdout),        bstr::BString::from(output.stderr),    );     open(dest_path)} /// Writes out gitlink entry pointing to the `target_repo`.pub fn create_gitlink(src_repo: impl AsRef<Path>, target_repo: impl AsRef<Path>) {    let git_link_path = src_repo.as_ref().join(".git");    std::fs::write(        git_link_path,        format!("gitdir: {}\n", target_repo.as_ref().display()),    )    .unwrap();} pub fn remove_config_value(mut repo: gix::Repository, section: &str, key: &str) {    let mut config = repo.config_snapshot_mut();    let Ok(mut section) = config.section_mut(section, None) else {        return;    };    section.remove(key);     let mut file = std::fs::File::create(config.meta().path.as_ref().unwrap()).unwrap();    config        .write_to_filter(&mut file, |section| section.meta() == config.meta())        .unwrap();} pub struct CommitResult {    pub tree_id: gix::ObjectId,    pub commit_id: gix::ObjectId,} pub fn add_commit(    repo: &gix::Repository,    reference: &str,    filename: &str,    content: &[u8],    message: &str,    parents: &[gix::ObjectId],) -> CommitResult {    let blob_oid = repo.write_blob(content).unwrap();     let parent_tree_editor = parents.first().map(|commit_id| {        repo.find_commit(*commit_id)            .unwrap()            .tree()            .unwrap()            .edit()            .unwrap()    });    let empty_tree_editor_fn = || {        repo.edit_tree(gix::ObjectId::empty_tree(repo.object_hash()))            .unwrap()    };     let mut tree_editor = parent_tree_editor.unwrap_or_else(empty_tree_editor_fn);    tree_editor        .upsert(filename, gix::object::tree::EntryKind::Blob, blob_oid)        .unwrap();    let tree_id = tree_editor.write().unwrap().detach();    let commit_id = write_commit(repo, reference, tree_id, message, parents);    CommitResult { tree_id, commit_id }} pub fn write_commit(    repo: &gix::Repository,    reference: &str,    tree_id: gix::ObjectId,    message: &str,    parents: &[gix::ObjectId],) -> gix::ObjectId {    let signature = signature();    repo.commit_as(        signature.to_ref(&mut TimeBuf::default()),        signature.to_ref(&mut TimeBuf::default()),        reference,        message,        tree_id,        parents.iter().copied(),    )    .unwrap()    .detach()} pub fn set_head_to_id(repo: &gix::Repository, target: gix::ObjectId) {    repo.edit_reference(gix::refs::transaction::RefEdit {        change: gix::refs::transaction::Change::Update {            log: gix::refs::transaction::LogChange::default(),            expected: gix::refs::transaction::PreviousValue::Any,            new: gix::refs::Target::Object(target),        },        name: "HEAD".try_into().unwrap(),        deref: false,    })    .unwrap();} pub fn set_symbolic_reference(repo: &gix::Repository, reference: &str, target: &str) {    use gix::refs::transaction;    let change = transaction::Change::Update {        log: transaction::LogChange {            mode: transaction::RefLog::AndReference,            force_create_reflog: true,            message: "create symbolic reference".into(),        },        expected: transaction::PreviousValue::Any,        new: gix::refs::Target::Symbolic(target.try_into().unwrap()),    };     let ref_edit = transaction::RefEdit {        change,        name: reference.try_into().unwrap(),        deref: false,    };    repo.edit_reference(ref_edit).unwrap();} pub fn checkout_tree_index(repo: &gix::Repository, tree_id: gix::ObjectId) {    let objects = repo.objects.clone();    let mut index = repo.index_from_tree(&tree_id).unwrap();    gix::worktree::state::checkout(        &mut index,        repo.workdir().unwrap(),        objects,        &gix::progress::Discard,        &gix::progress::Discard,        &gix::interrupt::IS_INTERRUPTED,        gix::worktree::state::checkout::Options::default(),    )    .unwrap();} fn signature() -> gix::actor::Signature {    gix::actor::Signature {        name: bstr::BString::from(GIT_USER),        email: bstr::BString::from(GIT_EMAIL),        time: gix::date::Time::new(0, 0),    }} #[derive(Debug, PartialEq, Eq)]pub enum GitStatusInfo {    Index(IndexStatus),    Worktree(WorktreeStatus),} #[derive(Debug, PartialEq, Eq)]pub enum IndexStatus {    Addition,    Deletion,    Rename,    Modification,} #[derive(Debug, PartialEq, Eq)]pub enum WorktreeStatus {    Removed,    Added,    Modified,    TypeChange,    Renamed,    Copied,    IntentToAdd,    Conflict,    Ignored,} impl<'lhs, 'rhs> From<gix::diff::index::ChangeRef<'lhs, 'rhs>> for IndexStatus {    fn from(value: gix::diff::index::ChangeRef<'lhs, 'rhs>) -> Self {        match value {            gix::diff::index::ChangeRef::Addition { .. } => IndexStatus::Addition,            gix::diff::index::ChangeRef::Deletion { .. } => IndexStatus::Deletion,            gix::diff::index::ChangeRef::Rewrite { .. } => IndexStatus::Rename,            gix::diff::index::ChangeRef::Modification { .. } => IndexStatus::Modification,        }    }} impl From<Option<gix::status::index_worktree::iter::Summary>> for WorktreeStatus {    fn from(value: Option<gix::status::index_worktree::iter::Summary>) -> Self {        match value {            Some(gix::status::index_worktree::iter::Summary::Removed) => WorktreeStatus::Removed,            Some(gix::status::index_worktree::iter::Summary::Added) => WorktreeStatus::Added,            Some(gix::status::index_worktree::iter::Summary::Modified) => WorktreeStatus::Modified,            Some(gix::status::index_worktree::iter::Summary::TypeChange) => {                WorktreeStatus::TypeChange            }            Some(gix::status::index_worktree::iter::Summary::Renamed) => WorktreeStatus::Renamed,            Some(gix::status::index_worktree::iter::Summary::Copied) => WorktreeStatus::Copied,            Some(gix::status::index_worktree::iter::Summary::IntentToAdd) => {                WorktreeStatus::IntentToAdd            }            Some(gix::status::index_worktree::iter::Summary::Conflict) => WorktreeStatus::Conflict,            None => WorktreeStatus::Ignored,        }    }} impl From<gix::status::Item> for GitStatusInfo {    fn from(value: gix::status::Item) -> Self {        match value {            gix::status::Item::TreeIndex(change) => GitStatusInfo::Index(change.into()),            gix::status::Item::IndexWorktree(item) => {                GitStatusInfo::Worktree(item.summary().into())            }        }    }} #[derive(Debug, PartialEq, Eq)]pub struct GitStatus {    path: String,    status: GitStatusInfo,} impl From<gix::status::Item> for GitStatus {    fn from(value: gix::status::Item) -> Self {        let path = value.location().to_string();        let status = value.into();        GitStatus { path, status }    }} pub fn status(repo: &gix::Repository) -> Vec<GitStatus> {    let mut status: Vec<GitStatus> = repo        .status(gix::progress::Discard)        .unwrap()        .untracked_files(gix::status::UntrackedFiles::Files)        .dirwalk_options(|options| {            options.emit_ignored(Some(gix::dir::walk::EmissionMode::Matching))        })        .into_iter(None)        .unwrap()        .map(Result::unwrap)        .map(|x| x.into())        .collect();     status.sort_by(|a, b| a.path.cmp(&b.path));    status} pub struct IndexManager<'a> {    index: gix::index::File,    repo: &'a gix::Repository,} impl<'a> IndexManager<'a> {    pub fn new(repo: &'a gix::Repository) -> IndexManager<'a> {        // This would be equivalent to repo.open_index_or_empty() if such        // function existed.        let index = repo.index_or_empty().unwrap();        let index = gix::index::File::clone(&index); // unshare        IndexManager { index, repo }    }     pub fn add_file(&mut self, name: &str, data: &[u8]) {        std::fs::write(self.repo.workdir().unwrap().join(name), data).unwrap();        let blob_oid = self.repo.write_blob(data).unwrap().detach();         self.index.dangerously_push_entry(            gix::index::entry::Stat::default(),            blob_oid,            gix::index::entry::Flags::from_stage(gix::index::entry::Stage::Unconflicted),            gix::index::entry::Mode::FILE,            name.as_bytes().into(),        );    }     pub fn sync_index(&mut self) {        self.index.sort_entries();        self.index.verify_entries().unwrap();        self.index            .write(gix::index::write::Options::default())            .unwrap();    }} pub fn add_remote(repo_dir: impl AsRef<Path>, remote_name: &str, url: &str) {    let output = std::process::Command::new("git")        .current_dir(repo_dir)        .args(["remote", "add", remote_name, url])        .output()        .unwrap();    assert!(        output.status.success(),        "git remote add {remote_name} {url} failed with {}:\n{}\n----- stderr -----\n{}",        output.status,        bstr::BString::from(output.stdout),        bstr::BString::from(output.stderr),    );} pub fn rename_remote(repo_dir: impl AsRef<Path>, original: &str, new: &str) {    let output = std::process::Command::new("git")        .current_dir(repo_dir)        .args(["remote", "rename", original, new])        .output()        .unwrap();    assert!(        output.status.success(),        "git remote rename failed with {}:\n{}\n----- stderr -----\n{}",        output.status,        bstr::BString::from(output.stdout),        bstr::BString::from(output.stderr),    );} pub fn fetch(repo_dir: impl AsRef<Path>, remote: &str) {    let output = std::process::Command::new("git")        .current_dir(repo_dir)        .args(["fetch", remote])        .output()        .unwrap();    assert!(        output.status.success(),        "git fetch {remote} failed with {}:\n{}\n----- stderr -----\n{}",        output.status,        bstr::BString::from(output.stdout),        bstr::BString::from(output.stderr),    );}