Skip to main content

jj_core/
ref_name.rs

1// Copyright 2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Name types for commit references.
16//!
17//! Name types can be constructed from a string:
18//! ```
19//! # use jj_core::ref_name::*;
20//! let _: RefNameBuf = "main".into();
21//! let _: &RemoteName = "origin".as_ref();
22//! ```
23//!
24//! However, they cannot be converted to other name types:
25//! ```compile_fail
26//! # use jj_core::ref_name::*;
27//! let _: RefNameBuf = RemoteName::new("origin").into();
28//! ```
29//! ```compile_fail
30//! # use jj_core::ref_name::*;
31//! let _: &RemoteName = RefName::new("main").as_ref();
32//! ```
33
34use std::borrow::Borrow;
35use std::fmt;
36use std::fmt::Display;
37use std::ops::Deref;
38
39use ref_cast::RefCastCustom;
40use ref_cast::ref_cast_custom;
41
42use crate::content_hash::ContentHash;
43use crate::symbol_util::format_string;
44
45/// Owned Git ref name in fully-qualified form (e.g. `refs/heads/main`.)
46///
47/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
48/// be considered an immutable `String`.
49// Eq, Hash, and Ord must be compatible with GitRefName.
50#[derive(Clone, ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct GitRefNameBuf(String);
52
53/// Borrowed Git ref name in fully-qualified form (e.g. `refs/heads/main`.)
54///
55/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
56/// be considered an immutable `str`.
57#[derive(ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
58#[repr(transparent)]
59pub struct GitRefName(str);
60
61/// Owned local (or local part of remote) bookmark or tag name.
62///
63/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
64/// be considered an immutable `String`.
65// Eq, Hash, and Ord must be compatible with RefName.
66#[derive(Clone, ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
67pub struct RefNameBuf(String);
68
69/// Borrowed local (or local part of remote) bookmark or tag name.
70///
71/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
72/// be considered an immutable `str`.
73#[derive(ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
74#[repr(transparent)]
75pub struct RefName(str);
76
77/// Owned remote name.
78///
79/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
80/// be considered an immutable `String`.
81// Eq, Hash, and Ord must be compatible with RemoteName.
82#[derive(Clone, ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct RemoteNameBuf(String);
84
85/// Borrowed remote name.
86///
87/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
88/// be considered an immutable `str`.
89#[derive(ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
90#[repr(transparent)]
91pub struct RemoteName(str);
92
93/// Owned workspace name.
94///
95/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
96/// be considered an immutable `String`.
97// Eq, Hash, and Ord must be compatible with WorkspaceName.
98#[derive(Clone, ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Serialize)]
99#[serde(transparent)]
100pub struct WorkspaceNameBuf(String);
101
102/// Borrowed workspace name.
103///
104/// Use `.as_str()` or `.as_symbol()` for displaying. Other than that, this can
105/// be considered an immutable `str`.
106#[derive(
107    ContentHash, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom, serde::Serialize,
108)]
109#[serde(transparent)]
110#[repr(transparent)]
111pub struct WorkspaceName(str);
112
113macro_rules! impl_partial_eq {
114    ($borrowed_ty:ty, $lhs:ty, $rhs:ty) => {
115        impl PartialEq<$rhs> for $lhs {
116            fn eq(&self, other: &$rhs) -> bool {
117                <$borrowed_ty as PartialEq>::eq(self, other)
118            }
119        }
120
121        impl PartialEq<$lhs> for $rhs {
122            fn eq(&self, other: &$lhs) -> bool {
123                <$borrowed_ty as PartialEq>::eq(self, other)
124            }
125        }
126    };
127}
128
129macro_rules! impl_partial_eq_str {
130    ($borrowed_ty:ty, $lhs:ty, $rhs:ty) => {
131        impl PartialEq<$rhs> for $lhs {
132            fn eq(&self, other: &$rhs) -> bool {
133                <$borrowed_ty as PartialEq>::eq(self, other.as_ref())
134            }
135        }
136
137        impl PartialEq<$lhs> for $rhs {
138            fn eq(&self, other: &$lhs) -> bool {
139                <$borrowed_ty as PartialEq>::eq(self.as_ref(), other)
140            }
141        }
142    };
143}
144
145macro_rules! impl_name_type {
146    ($owned_ty:ident, $borrowed_ty:ident) => {
147        impl $owned_ty {
148            /// Consumes this and returns the underlying string.
149            pub fn into_string(self) -> String {
150                self.0
151            }
152        }
153
154        impl $borrowed_ty {
155            /// Wraps string name.
156            #[ref_cast_custom]
157            pub const fn new(name: &str) -> &Self;
158
159            /// Returns the underlying string.
160            pub const fn as_str(&self) -> &str {
161                &self.0
162            }
163
164            /// Converts to symbol for displaying.
165            pub fn as_symbol(&self) -> &RefSymbol {
166                RefSymbol::new(&self.0)
167            }
168        }
169
170        // Owned type can be constructed from (weakly-typed) string:
171
172        impl From<String> for $owned_ty {
173            fn from(value: String) -> Self {
174                $owned_ty(value)
175            }
176        }
177
178        impl From<&String> for $owned_ty {
179            fn from(value: &String) -> Self {
180                $owned_ty(value.clone())
181            }
182        }
183
184        impl From<&str> for $owned_ty {
185            fn from(value: &str) -> Self {
186                $owned_ty(value.to_owned())
187            }
188        }
189
190        // Owned type can be constructed from borrowed type:
191
192        impl From<&$owned_ty> for $owned_ty {
193            fn from(value: &$owned_ty) -> Self {
194                value.clone()
195            }
196        }
197
198        impl From<&$borrowed_ty> for $owned_ty {
199            fn from(value: &$borrowed_ty) -> Self {
200                value.to_owned()
201            }
202        }
203
204        // Borrowed type can be constructed from (weakly-typed) string:
205
206        impl AsRef<$borrowed_ty> for String {
207            fn as_ref(&self) -> &$borrowed_ty {
208                $borrowed_ty::new(self)
209            }
210        }
211
212        impl AsRef<$borrowed_ty> for str {
213            fn as_ref(&self) -> &$borrowed_ty {
214                $borrowed_ty::new(self)
215            }
216        }
217
218        // Types can be converted to (weakly-typed) string:
219
220        impl From<$owned_ty> for String {
221            fn from(value: $owned_ty) -> Self {
222                value.0
223            }
224        }
225
226        impl From<&$owned_ty> for String {
227            fn from(value: &$owned_ty) -> Self {
228                value.0.clone()
229            }
230        }
231
232        impl From<&$borrowed_ty> for String {
233            fn from(value: &$borrowed_ty) -> Self {
234                value.0.to_owned()
235            }
236        }
237
238        impl AsRef<str> for $owned_ty {
239            fn as_ref(&self) -> &str {
240                self.as_str()
241            }
242        }
243
244        impl AsRef<str> for $borrowed_ty {
245            fn as_ref(&self) -> &str {
246                self.as_str()
247            }
248        }
249
250        // Types can be converted to borrowed type, and back to owned type:
251
252        impl AsRef<$borrowed_ty> for $owned_ty {
253            fn as_ref(&self) -> &$borrowed_ty {
254                self
255            }
256        }
257
258        impl AsRef<$borrowed_ty> for $borrowed_ty {
259            fn as_ref(&self) -> &$borrowed_ty {
260                self
261            }
262        }
263
264        impl Borrow<$borrowed_ty> for $owned_ty {
265            fn borrow(&self) -> &$borrowed_ty {
266                self
267            }
268        }
269
270        impl Deref for $owned_ty {
271            type Target = $borrowed_ty;
272
273            fn deref(&self) -> &Self::Target {
274                $borrowed_ty::new(&self.0)
275            }
276        }
277
278        impl ToOwned for $borrowed_ty {
279            type Owned = $owned_ty;
280
281            fn to_owned(&self) -> Self::Owned {
282                $owned_ty(self.0.to_owned())
283            }
284        }
285
286        // Owned and borrowed types can be compared:
287        impl_partial_eq!($borrowed_ty, $owned_ty, $borrowed_ty);
288        impl_partial_eq!($borrowed_ty, $owned_ty, &$borrowed_ty);
289
290        // Types can be compared with (weakly-typed) string:
291        impl_partial_eq_str!($borrowed_ty, $owned_ty, str);
292        impl_partial_eq_str!($borrowed_ty, $owned_ty, &str);
293        impl_partial_eq_str!($borrowed_ty, $owned_ty, String);
294        impl_partial_eq_str!($borrowed_ty, $borrowed_ty, str);
295        impl_partial_eq_str!($borrowed_ty, $borrowed_ty, &str);
296        impl_partial_eq_str!($borrowed_ty, $borrowed_ty, String);
297        impl_partial_eq_str!($borrowed_ty, &$borrowed_ty, str);
298        impl_partial_eq_str!($borrowed_ty, &$borrowed_ty, String);
299    };
300}
301
302impl_name_type!(GitRefNameBuf, GitRefName);
303// TODO: split RefName into BookmarkName and TagName? That will make sense at
304// repo/view API surface, but we'll need generic RemoteRefSymbol type, etc.
305impl_name_type!(RefNameBuf, RefName);
306impl_name_type!(RemoteNameBuf, RemoteName);
307impl_name_type!(WorkspaceNameBuf, WorkspaceName);
308
309impl RefName {
310    /// Constructs a remote symbol with this local name.
311    pub fn to_remote_symbol<'a>(&'a self, remote: &'a RemoteName) -> RemoteRefSymbol<'a> {
312        RemoteRefSymbol { name: self, remote }
313    }
314}
315
316impl WorkspaceName {
317    /// Default workspace name.
318    pub const DEFAULT: &Self = Self::new("default");
319}
320
321/// Symbol for displaying.
322///
323/// This type can be displayed with quoting and escaping if necessary.
324#[derive(Debug, RefCastCustom)]
325#[repr(transparent)]
326pub struct RefSymbol(str);
327
328impl RefSymbol {
329    /// Wraps string name.
330    #[ref_cast_custom]
331    const fn new(name: &str) -> &Self;
332}
333
334impl Display for RefSymbol {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        if is_safe_identifier(&self.0) {
337            f.pad(&self.0)
338        } else {
339            f.pad(&format_string(&self.0))
340        }
341    }
342}
343
344/// Owned remote bookmark or tag name.
345///
346/// This type can be displayed in `{name}@{remote}` form, with quoting and
347/// escaping if necessary.
348#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
349pub struct RemoteRefSymbolBuf {
350    /// Local name.
351    pub name: RefNameBuf,
352    /// Remote name.
353    pub remote: RemoteNameBuf,
354}
355
356impl RemoteRefSymbolBuf {
357    /// Converts to reference type.
358    pub fn as_ref(&self) -> RemoteRefSymbol<'_> {
359        RemoteRefSymbol {
360            name: &self.name,
361            remote: &self.remote,
362        }
363    }
364}
365
366/// Borrowed remote bookmark or tag name.
367///
368/// This type can be displayed in `{name}@{remote}` form, with quoting and
369/// escaping if necessary.
370#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
371pub struct RemoteRefSymbol<'a> {
372    /// Local name.
373    pub name: &'a RefName,
374    /// Remote name.
375    pub remote: &'a RemoteName,
376}
377
378impl RemoteRefSymbol<'_> {
379    /// Converts to owned type.
380    pub fn to_owned(self) -> RemoteRefSymbolBuf {
381        RemoteRefSymbolBuf {
382            name: self.name.to_owned(),
383            remote: self.remote.to_owned(),
384        }
385    }
386}
387
388impl From<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf {
389    fn from(value: RemoteRefSymbol<'_>) -> Self {
390        value.to_owned()
391    }
392}
393
394impl PartialEq<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf {
395    fn eq(&self, other: &RemoteRefSymbol) -> bool {
396        self.as_ref() == *other
397    }
398}
399
400impl PartialEq<RemoteRefSymbol<'_>> for &RemoteRefSymbolBuf {
401    fn eq(&self, other: &RemoteRefSymbol) -> bool {
402        self.as_ref() == *other
403    }
404}
405
406impl PartialEq<RemoteRefSymbolBuf> for RemoteRefSymbol<'_> {
407    fn eq(&self, other: &RemoteRefSymbolBuf) -> bool {
408        *self == other.as_ref()
409    }
410}
411
412impl PartialEq<&RemoteRefSymbolBuf> for RemoteRefSymbol<'_> {
413    fn eq(&self, other: &&RemoteRefSymbolBuf) -> bool {
414        *self == other.as_ref()
415    }
416}
417
418impl Display for RemoteRefSymbolBuf {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        Display::fmt(&self.as_ref(), f)
421    }
422}
423
424impl Display for RemoteRefSymbol<'_> {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        let RemoteRefSymbol { name, remote } = self;
427        f.pad(&format!("{}@{}", name.as_symbol(), remote.as_symbol()))
428    }
429}
430
431/// Returns `true` if the `symbol` never requires quoting in revsets.
432///
433/// Note that this check is conservative; it may return `false` for some valid
434/// unquoted symbols.
435fn is_safe_identifier(symbol: &str) -> bool {
436    // Based on the strict_identifier rule in revset.pest
437    let is_safe_byte = |b: u8| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'/');
438    symbol
439        .split(&['.', '-', '+'])
440        .all(|part| !part.is_empty() && part.as_bytes().iter().copied().all(is_safe_byte))
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_symbol_formatting() {
449        assert_eq!(RefSymbol::new("").to_string(), r#""""#);
450        assert_eq!(RefSymbol::new("foo").to_string(), "foo");
451        assert_eq!(RefSymbol::new("..").to_string(), r#""..""#);
452        assert_eq!(RefSymbol::new("柔術").to_string(), r#""柔術""#);
453        assert_eq!(
454            RemoteRefSymbol {
455                name: "foo".as_ref(),
456                remote: "bar".as_ref()
457            }
458            .to_string(),
459            "foo@bar"
460        );
461        assert_eq!(
462            RemoteRefSymbol {
463                name: ".".as_ref(),
464                remote: "-".as_ref()
465            }
466            .to_string(),
467            r#""."@"-""#
468        );
469    }
470
471    #[test]
472    fn test_is_safe_identifier() {
473        // Empty symbol requires quoting
474        assert!(!is_safe_identifier(""));
475        // Integer is a symbol
476        assert!(is_safe_identifier("0"));
477        // Tag/bookmark name separated by /
478        assert!(is_safe_identifier("foo_bar/baz"));
479        // Glob literal with star: rejected by the strict_identifier rule
480        assert!(!is_safe_identifier("*/foo/**"));
481
482        // Internal '.', '-', and '+': accepted
483        assert!(is_safe_identifier("foo.bar-v1+7"));
484        // '.', '-', and '+' at the beginning or end: rejected
485        assert!(!is_safe_identifier(".foo"));
486        assert!(!is_safe_identifier("foo."));
487        assert!(!is_safe_identifier("-foo"));
488        assert!(!is_safe_identifier("foo-"));
489        assert!(!is_safe_identifier("+foo"));
490        assert!(!is_safe_identifier("foo+"));
491        // Multiple '.', '-', and '+': rejected
492        assert!(!is_safe_identifier("foo--bar"));
493        assert!(!is_safe_identifier("foo.+bar"));
494        assert!(!is_safe_identifier("foo++bar"));
495        assert!(!is_safe_identifier("foo+-bar"));
496
497        // Non-ASCII tag/bookmark name: rejected by the strict_identifier rule
498        assert!(!is_safe_identifier("柔術"));
499    }
500}