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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Git's `core.logAllRefUpdates` policy for deciding which reference updates get reflog records.
use thiserror::Error;
use super::Repository;
use crate::Signature;
use crate::config::InvalidValue;
use crate::refs::{RefName, ReferenceError, Reflog};
/// A reflog policy could not be decided.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ReflogPolicyError {
/// `core.logAllRefUpdates` is not a boolean or `always`.
#[error("invalid reflog configuration")]
Config(#[source] InvalidValue),
/// Checking for an existing log failed.
#[error("checking for an existing reflog")]
Reference(#[source] ReferenceError),
}
/// Namespaces `core.logAllRefUpdates = true` logs, besides `HEAD`.
pub(crate) const CONVENTIONAL: [&[u8]; 3] = [b"refs/heads/", b"refs/remotes/", b"refs/notes/"];
/// Which references get new reflogs; a reference with an existing log is always logged.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ReflogScope {
/// `always`: every reference.
All,
/// `true`, or unset in a non-bare repository: `HEAD` and the [`CONVENTIONAL`] namespaces.
Conventional,
/// `false`, or unset in a bare repository: only references that already have a log.
Existing,
}
impl Repository {
/// Returns whether Git would record an update of `name` in its reflog.
///
/// Follows `core.logAllRefUpdates` as observed with `git update-ref`:
///
/// - `always` (any case) logs every reference.
/// - `true`, or no setting in a non-bare repository, logs `HEAD` and references under
/// `refs/heads/`, `refs/remotes/` and `refs/notes/`.
/// - `false`, or no setting in a bare repository, creates no logs.
///
/// A reference that already has a log is always logged, whatever the setting. Callers pass
/// [`crate::refs::Reflog::AppendIfChanged`] when this returns `true` and
/// [`crate::refs::Reflog::Preserve`] otherwise; [`Self::reflog_for`] makes that choice.
///
/// # Errors
///
/// Returns [`ReflogPolicyError::Config`] for a value Git rejects, including a variable
/// without `=`, and [`ReflogPolicyError::Reference`] when the existing log can't be checked.
pub fn logs_updates_to(&self, name: &RefName) -> Result<bool, ReflogPolicyError> {
let logged = match self.reflog_scope()? {
ReflogScope::All => true,
ReflogScope::Conventional => {
let bytes = name.as_bytes();
bytes == b"HEAD" || CONVENTIONAL.iter().any(|prefix| bytes.starts_with(prefix))
}
ReflogScope::Existing => false,
};
if logged {
return Ok(true);
}
self.references()
.and_then(|references| references.has_reflog(name))
.map_err(ReflogPolicyError::Reference)
}
/// Which references `core.logAllRefUpdates` logs, before counting existing logs.
pub(crate) fn reflog_scope(&self) -> Result<ReflogScope, ReflogPolicyError> {
let config = self.config();
let enabled = match config.value("core", None, "logallrefupdates") {
None => !self.is_bare(),
// Git rejects the implicit `true` of a `logAllRefUpdates` line without a value.
Some(None) => {
return Err(ReflogPolicyError::Config(InvalidValue::new(
"core",
"logallrefupdates",
None,
)));
}
Some(Some(value)) if value.eq_ignore_ascii_case(b"always") => {
return Ok(ReflogScope::All);
}
Some(Some(_)) => config
.boolean("core", None, "logallrefupdates")
.map_err(ReflogPolicyError::Config)?
.unwrap_or(false),
};
Ok(if enabled {
ReflogScope::Conventional
} else {
ReflogScope::Existing
})
}
/// Chooses the reflog policy Git would use for an update of `name`.
///
/// Returns [`Reflog::AppendIfChanged`] with `committer` and `message` when
/// [`Self::logs_updates_to`] returns `true`, and [`Reflog::Preserve`] otherwise. Like
/// `git update-ref -m <message>`, an update that leaves the value unchanged records nothing.
///
/// The policy applies to every name a [`crate::refs::Dereference::Follow`] edit logs. Git
/// decides per name, so pass the chain's terminal name (from
/// [`crate::refs::References::resolve`]) when editing through an alias: a branch reached
/// through a symbolic name without a log of its own is then still logged.
///
/// # Errors
///
/// Returns the errors of [`Self::logs_updates_to`].
///
/// ```
/// use girt::refs::{RefEdit, RefName, Reflog};
/// use girt::{InitKind, ObjectFormat, Repository, Signature};
///
/// let directory = tempfile::tempdir()?;
/// let repo = Repository::init(
/// ObjectFormat::Sha1,
/// directory.path().join("work"),
/// InitKind::Worktree,
/// )?;
///
/// let id = repo.loose_objects().write_blob(b"tip")?;
/// let who = Signature {
/// name: b"A. Writer".to_vec(),
/// email: b"writer@example.com".to_vec(),
/// seconds: 1_700_000_000,
/// offset_minutes: 0,
/// };
///
/// // A non-bare repository logs branches but not tags by default.
/// let branch = RefName::branch("topic")?;
/// let reflog = repo.reflog_for(&branch, who.clone(), "branch: Created")?;
/// assert!(matches!(reflog, Reflog::AppendIfChanged { .. }));
/// assert_eq!(
/// repo.reflog_for(&RefName::tag("v1")?, who, "tag")?,
/// Reflog::Preserve
/// );
///
/// let refs = repo.references()?;
/// refs.transaction(&[RefEdit::create(branch.clone(), id, reflog)])?;
/// assert_eq!(refs.reflog(&branch)?.map(|log| log.len()), Some(1));
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn reflog_for(
&self,
name: &RefName,
committer: Signature,
message: impl Into<Vec<u8>>,
) -> Result<Reflog, ReflogPolicyError> {
Ok(if self.logs_updates_to(name)? {
Reflog::AppendIfChanged {
committer,
message: message.into(),
}
} else {
Reflog::Preserve
})
}
}