Skip to main content

branchless/git/
index.rs

1use std::path::{Path, PathBuf};
2
3use eyre::Context;
4use tracing::instrument;
5
6use crate::core::eventlog::EventTransactionId;
7
8use super::{FileMode, GitRunInfo, GitRunOpts, GitRunResult, MaybeZeroOid, NonZeroOid, Repo, Tree};
9
10/// The possible stages for items in the index.
11#[derive(Copy, Clone, Debug)]
12pub enum Stage {
13    /// Normal staged change.
14    Stage0,
15
16    /// For a merge conflict, the contents of the file at the common ancestor of the merged commits.
17    Stage1,
18
19    /// "Our" changes.
20    Stage2,
21
22    /// "Their" changes (from the commit being merged in).
23    Stage3,
24}
25
26impl Stage {
27    pub(super) fn get_trailer(&self) -> &'static str {
28        match self {
29            Stage::Stage0 => "Branchless-stage-0",
30            Stage::Stage1 => "Branchless-stage-1",
31            Stage::Stage2 => "Branchless-stage-2",
32            Stage::Stage3 => "Branchless-stage-3",
33        }
34    }
35}
36
37impl From<Stage> for i32 {
38    fn from(stage: Stage) -> Self {
39        match stage {
40            Stage::Stage0 => 0,
41            Stage::Stage1 => 1,
42            Stage::Stage2 => 2,
43            Stage::Stage3 => 3,
44        }
45    }
46}
47
48/// An entry in the Git index.
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50pub struct IndexEntry {
51    pub(super) oid: MaybeZeroOid,
52    pub(super) file_mode: FileMode,
53}
54
55/// The Git index.
56pub struct Index {
57    pub(super) inner: git2::Index,
58}
59
60impl std::fmt::Debug for Index {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "<Index>")
63    }
64}
65
66impl Index {
67    /// Whether or not there are unresolved merge conflicts in the index.
68    pub fn has_conflicts(&self) -> bool {
69        self.inner.has_conflicts()
70    }
71
72    /// Get the (stage 0) entry for the given path.
73    pub fn get_entry(&self, path: &Path) -> Option<IndexEntry> {
74        self.get_entry_in_stage(path, Stage::Stage0)
75    }
76
77    /// Get the entry for the given path in the given stage.
78    pub fn get_entry_in_stage(&self, path: &Path, stage: Stage) -> Option<IndexEntry> {
79        self.inner
80            .get_path(path, i32::from(stage))
81            .map(|entry| IndexEntry {
82                oid: entry.id.into(),
83                file_mode: {
84                    // `libgit2` uses u32 for file modes in index entries, but
85                    // i32 for file modes in tree entries for some reason.
86                    let mode = i32::try_from(entry.mode).unwrap();
87                    FileMode::from(mode)
88                },
89            })
90    }
91
92    /// Update the index from the given tree and write it to disk.
93    pub fn update_from_tree(&mut self, tree: &Tree) -> eyre::Result<()> {
94        self.inner.read_tree(&tree.inner)?;
95        self.inner.write().wrap_err("writing index")
96    }
97}
98
99/// The command to update the index, as defined by `git update-index`.
100#[allow(missing_docs)]
101#[derive(Clone, Debug)]
102pub enum UpdateIndexCommand {
103    Delete {
104        path: PathBuf,
105    },
106    Update {
107        path: PathBuf,
108        stage: Stage,
109        mode: FileMode,
110        oid: NonZeroOid,
111    },
112}
113
114/// Update the index. This handles updates to stages other than 0.
115///
116/// libgit2 doesn't offer a good way of updating the index for higher stages, so
117/// internally we use `git update-index` directly.
118#[instrument]
119pub fn update_index(
120    git_run_info: &GitRunInfo,
121    repo: &Repo,
122    index: &Index,
123    event_tx_id: EventTransactionId,
124    commands: &[UpdateIndexCommand],
125) -> eyre::Result<()> {
126    let stdin = {
127        let mut buf = Vec::new();
128        for command in commands {
129            use std::io::Write;
130
131            match command {
132                UpdateIndexCommand::Delete { path } => {
133                    write!(
134                        &mut buf,
135                        "0 {zero} 0\t{path}\0",
136                        zero = MaybeZeroOid::Zero,
137                        path = path.display(),
138                    )?;
139                }
140
141                UpdateIndexCommand::Update {
142                    path,
143                    stage,
144                    mode,
145                    oid,
146                } => {
147                    write!(
148                        &mut buf,
149                        "{mode} {sha1} {stage}\t{path}\0",
150                        sha1 = oid,
151                        stage = i32::from(*stage),
152                        path = path.display(),
153                    )?;
154                }
155            }
156        }
157        buf
158    };
159
160    let GitRunResult { .. } = git_run_info
161        .run_silent(
162            repo,
163            Some(event_tx_id),
164            &["update-index", "-z", "--index-info"],
165            GitRunOpts {
166                treat_git_failure_as_error: true,
167                stdin: Some(stdin),
168            },
169        )
170        .wrap_err("Updating index")?;
171    Ok(())
172}