aboutsummaryrefslogtreecommitdiff
path: root/helix-vcs/src/git.rs
blob: 1732bdd0d3dd9b29fbf4235fa69d6952aae847e3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use arc_swap::ArcSwap;
use std::path::Path;
use std::sync::Arc;

use gix::objs::tree::EntryMode;
use gix::sec::trust::DefaultForLevel;
use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};

use crate::DiffProvider;

#[cfg(test)]
mod test;

pub struct Git;

impl Git {
    fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> {
        // custom open options
        let mut git_open_opts_map = gix::sec::trust::Mapping::<gix::open::Options>::default();

        // On windows various configuration options are bundled as part of the installations
        // This path depends on the install location of git and therefore requires some overhead to lookup
        // This is basically only used on windows and has some overhead hence it's disabled on other platforms.
        // `gitoxide` doesn't use this as default
        let config = gix::permissions::Config {
            system: true,
            git: true,
            user: true,
            env: true,
            includes: true,
            git_binary: cfg!(windows),
        };
        // change options for config permissions without touching anything else
        git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(gix::Permissions {
            config,
            ..gix::Permissions::default_for_level(gix::sec::Trust::Reduced)
        });
        git_open_opts_map.full = git_open_opts_map.full.permissions(gix::Permissions {
            config,
            ..gix::Permissions::default_for_level(gix::sec::Trust::Full)
        });

        let mut open_options = gix::discover::upwards::Options::default();
        if let Some(ceiling_dir) = ceiling_dir {
            open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
        }

        ThreadSafeRepository::discover_with_environment_overrides_opts(
            path,
            open_options,
            git_open_opts_map,
        )
        .ok()
    }
}

impl DiffProvider for Git {
    fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
        debug_assert!(!file.exists() || file.is_file());
        debug_assert!(file.is_absolute());

        // TODO cache repository lookup
        let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
        let head = repo.head_commit().ok()?;
        let file_oid = find_file_in_commit(&repo, &head, file)?;

        let file_object = repo.find_object(file_oid).ok()?;
        let mut data = file_object.detach().data;
        // convert LF to CRLF if configured to avoid showing every line as changed
        if repo
            .config_snapshot()
            .boolean("core.autocrlf")
            .unwrap_or(false)
        {
            let mut normalized_file = Vec::with_capacity(data.len());
            let mut at_cr = false;
            for &byte in &data {
                if byte == b'\n' {
                    // if this is a LF instead of a CRLF (last byte was not a CR)
                    // insert a new CR to generate a CRLF
                    if !at_cr {
                        normalized_file.push(b'\r');
                    }
                }
                at_cr = byte == b'\r';
                normalized_file.push(byte)
            }
            data = normalized_file
        }
        Some(data)
    }

    fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> {
        debug_assert!(!file.exists() || file.is_file());
        debug_assert!(file.is_absolute());
        let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
        let head_ref = repo.head_ref().ok()?;
        let head_commit = repo.head_commit().ok()?;

        let name = match head_ref {
            Some(reference) => reference.name().shorten().to_string(),
            None => head_commit.id.to_hex_with_len(8).to_string(),
        };

        Some(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
    }
}

/// Finds the object that contains the contents of a file at a specific commit.
fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> {
    let repo_dir = repo.work_dir()?;
    let rel_path = file.strip_prefix(repo_dir).ok()?;
    let tree = commit.tree().ok()?;
    let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??;
    match tree_entry.mode() {
        // not a file, everything is new, do not show diff
        EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None,
        // found a file
        EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()),
    }
}