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};
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
93/// The command to update the index, as defined by `git update-index`.
94#[allow(missing_docs)]
95#[derive(Clone, Debug)]
96pub enum UpdateIndexCommand {
97    Delete {
98        path: PathBuf,
99    },
100    Update {
101        path: PathBuf,
102        stage: Stage,
103        mode: FileMode,
104        oid: NonZeroOid,
105    },
106}
107
108/// Update the index. This handles updates to stages other than 0.
109///
110/// libgit2 doesn't offer a good way of updating the index for higher stages, so
111/// internally we use `git update-index` directly.
112#[instrument]
113pub fn update_index(
114    git_run_info: &GitRunInfo,
115    repo: &Repo,
116    index: &Index,
117    event_tx_id: EventTransactionId,
118    commands: &[UpdateIndexCommand],
119) -> eyre::Result<()> {
120    let stdin = {
121        let mut buf = Vec::new();
122        for command in commands {
123            use std::io::Write;
124
125            match command {
126                UpdateIndexCommand::Delete { path } => {
127                    write!(
128                        &mut buf,
129                        "0 {zero} 0\t{path}\0",
130                        zero = MaybeZeroOid::Zero,
131                        path = path.display(),
132                    )?;
133                }
134
135                UpdateIndexCommand::Update {
136                    path,
137                    stage,
138                    mode,
139                    oid,
140                } => {
141                    write!(
142                        &mut buf,
143                        "{mode} {sha1} {stage}\t{path}\0",
144                        sha1 = oid,
145                        stage = i32::from(*stage),
146                        path = path.display(),
147                    )?;
148                }
149            }
150        }
151        buf
152    };
153
154    let GitRunResult { .. } = git_run_info
155        .run_silent(
156            repo,
157            Some(event_tx_id),
158            &["update-index", "-z", "--index-info"],
159            GitRunOpts {
160                treat_git_failure_as_error: true,
161                stdin: Some(stdin),
162            },
163        )
164        .wrap_err("Updating index")?;
165    Ok(())
166}