4650
Cargo.lock
generated
4650
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -53,6 +53,7 @@ members = [
|
||||
"transaction-status",
|
||||
"upload-perf",
|
||||
"net-utils",
|
||||
"version",
|
||||
"vote-signer",
|
||||
"cli",
|
||||
"rayon-threadlimit",
|
||||
|
@ -108,6 +108,8 @@ pub struct RpcContactInfo {
|
||||
pub tpu: Option<SocketAddr>,
|
||||
/// JSON RPC port
|
||||
pub rpc: Option<SocketAddr>,
|
||||
/// Software version
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
/// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot
|
||||
|
@ -61,6 +61,7 @@ solana-runtime = { path = "../runtime", version = "1.0.23" }
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
solana-stake-program = { path = "../programs/stake", version = "1.0.23" }
|
||||
solana-storage-program = { path = "../programs/storage", version = "1.0.23" }
|
||||
solana-version = { path = "../version", version = "1.0.23" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "1.0.23" }
|
||||
solana-vote-signer = { path = "../vote-signer", version = "1.0.23" }
|
||||
solana-sys-tuner = { path = "../sys-tuner", version = "1.0.23" }
|
||||
|
@ -20,7 +20,8 @@ use crate::{
|
||||
crds_gossip_error::CrdsGossipError,
|
||||
crds_gossip_pull::{CrdsFilter, CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS},
|
||||
crds_value::{
|
||||
self, CrdsData, CrdsValue, CrdsValueLabel, EpochSlots, SnapshotHash, Vote, MAX_WALLCLOCK,
|
||||
self, CrdsData, CrdsValue, CrdsValueLabel, EpochSlots, SnapshotHash, Version, Vote,
|
||||
MAX_WALLCLOCK,
|
||||
},
|
||||
packet::{Packet, PACKET_DATA_SIZE},
|
||||
result::{Error, Result},
|
||||
@ -336,6 +337,7 @@ impl ClusterInfo {
|
||||
archivers += 1;
|
||||
}
|
||||
|
||||
let node_version = self.get_node_version(&node.id);
|
||||
if my_shred_version != 0 && (node.shred_version != 0 && node.shred_version != my_shred_version) {
|
||||
different_shred_nodes += 1;
|
||||
None
|
||||
@ -351,10 +353,9 @@ impl ClusterInfo {
|
||||
"none".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
let ip_addr = node.gossip.ip();
|
||||
Some(format!(
|
||||
"{:15} {:2}| {:5} | {:44} | {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n",
|
||||
"{:15} {:2}| {:5} | {:44} |{:^15}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n",
|
||||
if ContactInfo::is_valid_address(&node.gossip) {
|
||||
ip_addr.to_string()
|
||||
} else {
|
||||
@ -363,6 +364,11 @@ impl ClusterInfo {
|
||||
if node.id == my_pubkey { "me" } else { "" }.to_string(),
|
||||
now.saturating_sub(last_updated),
|
||||
node.id.to_string(),
|
||||
if let Some(node_version) = node_version {
|
||||
node_version.to_string()
|
||||
} else {
|
||||
"-".to_string()
|
||||
},
|
||||
addr_to_string(&ip_addr, &node.gossip),
|
||||
addr_to_string(&ip_addr, &node.tpu),
|
||||
addr_to_string(&ip_addr, &node.tpu_forwards),
|
||||
@ -370,7 +376,6 @@ impl ClusterInfo {
|
||||
addr_to_string(&ip_addr, &node.tvu_forwards),
|
||||
addr_to_string(&ip_addr, &node.repair),
|
||||
addr_to_string(&ip_addr, &node.serve_repair),
|
||||
addr_to_string(&ip_addr, &node.storage_addr),
|
||||
addr_to_string(&ip_addr, &node.rpc),
|
||||
addr_to_string(&ip_addr, &node.rpc_pubsub),
|
||||
node.shred_version,
|
||||
@ -381,9 +386,9 @@ impl ClusterInfo {
|
||||
|
||||
format!(
|
||||
"IP Address |Age(ms)| Node identifier \
|
||||
|Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR|Storag| RPC |PubSub|ShredVer\n\
|
||||
------------------+-------+----------------------------------------------+\
|
||||
------+------+------+------+------+------+------+------+------+------+--------\n\
|
||||
| Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\
|
||||
------------------+-------+----------------------------------------------+---------------+\
|
||||
------+------+------+------+------+------+------+------+------+--------\n\
|
||||
{}\
|
||||
Nodes: {}{}{}{}",
|
||||
nodes.join(""),
|
||||
@ -398,7 +403,7 @@ impl ClusterInfo {
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
if spy_nodes > 0 {
|
||||
if different_shred_nodes > 0 {
|
||||
format!(
|
||||
"\nNodes with different shred version: {}",
|
||||
different_shred_nodes
|
||||
@ -556,6 +561,16 @@ impl ClusterInfo {
|
||||
.map(|x| x.value.contact_info().unwrap())
|
||||
}
|
||||
|
||||
pub fn get_node_version(&self, pubkey: &Pubkey) -> Option<solana_version::Version> {
|
||||
self.gossip
|
||||
.crds
|
||||
.table
|
||||
.get(&CrdsValueLabel::Version(*pubkey))
|
||||
.map(|x| x.value.version())
|
||||
.flatten()
|
||||
.map(|version| version.version.clone())
|
||||
}
|
||||
|
||||
/// all validators that have a valid rpc port regardless of `shred_version`.
|
||||
pub fn all_rpc_peers(&self) -> Vec<ContactInfo> {
|
||||
let me = self.my_data();
|
||||
@ -1201,6 +1216,14 @@ impl ClusterInfo {
|
||||
let mut last_contact_info_trace = timestamp();
|
||||
let mut adopt_shred_version = obj.read().unwrap().my_data().shred_version == 0;
|
||||
let recycler = PacketsRecycler::default();
|
||||
{
|
||||
let mut obj = obj.write().unwrap();
|
||||
let message = CrdsValue::new_signed(
|
||||
CrdsData::Version(Version::new(obj.id())),
|
||||
&obj.keypair,
|
||||
);
|
||||
obj.push_message(message);
|
||||
}
|
||||
loop {
|
||||
let start = timestamp();
|
||||
thread_mem_usage::datapoint("solana-gossip");
|
||||
|
@ -71,6 +71,7 @@ pub enum CrdsData {
|
||||
EpochSlots(EpochSlotIndex, EpochSlots),
|
||||
SnapshotHashes(SnapshotHash),
|
||||
AccountsHashes(SnapshotHash),
|
||||
Version(Version),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
@ -122,6 +123,7 @@ impl Sanitize for CrdsData {
|
||||
}
|
||||
val.sanitize()
|
||||
}
|
||||
CrdsData::Version(version) => version.sanitize(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,6 +230,33 @@ impl Vote {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct Version {
|
||||
pub from: Pubkey,
|
||||
pub wallclock: u64,
|
||||
pub version: solana_version::Version,
|
||||
}
|
||||
|
||||
impl Sanitize for Version {
|
||||
fn sanitize(&self) -> Result<(), SanitizeError> {
|
||||
if self.wallclock >= MAX_WALLCLOCK {
|
||||
return Err(SanitizeError::ValueOutOfBounds);
|
||||
}
|
||||
self.from.sanitize()?;
|
||||
self.version.sanitize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn new(from: Pubkey) -> Self {
|
||||
Self {
|
||||
from,
|
||||
wallclock: timestamp(),
|
||||
version: solana_version::Version::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of the replicated value
|
||||
/// These are labels for values in a record that is associated with `Pubkey`
|
||||
#[derive(PartialEq, Hash, Eq, Clone, Debug)]
|
||||
@ -237,6 +266,7 @@ pub enum CrdsValueLabel {
|
||||
EpochSlots(Pubkey),
|
||||
SnapshotHashes(Pubkey),
|
||||
AccountsHashes(Pubkey),
|
||||
Version(Pubkey),
|
||||
}
|
||||
|
||||
impl fmt::Display for CrdsValueLabel {
|
||||
@ -247,6 +277,7 @@ impl fmt::Display for CrdsValueLabel {
|
||||
CrdsValueLabel::EpochSlots(_) => write!(f, "EpochSlots({})", self.pubkey()),
|
||||
CrdsValueLabel::SnapshotHashes(_) => write!(f, "SnapshotHashes({})", self.pubkey()),
|
||||
CrdsValueLabel::AccountsHashes(_) => write!(f, "AccountsHashes({})", self.pubkey()),
|
||||
CrdsValueLabel::Version(_) => write!(f, "Version({})", self.pubkey()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -259,6 +290,7 @@ impl CrdsValueLabel {
|
||||
CrdsValueLabel::EpochSlots(p) => *p,
|
||||
CrdsValueLabel::SnapshotHashes(p) => *p,
|
||||
CrdsValueLabel::AccountsHashes(p) => *p,
|
||||
CrdsValueLabel::Version(p) => *p,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -276,7 +308,7 @@ impl CrdsValue {
|
||||
value.sign(keypair);
|
||||
value
|
||||
}
|
||||
/// Totally unsecure unverfiable wallclock of the node that generated this message
|
||||
/// Totally unsecure unverifiable wallclock of the node that generated this message
|
||||
/// Latest wallclock is always picked.
|
||||
/// This is used to time out push messages.
|
||||
pub fn wallclock(&self) -> u64 {
|
||||
@ -286,6 +318,7 @@ impl CrdsValue {
|
||||
CrdsData::EpochSlots(_, vote) => vote.wallclock,
|
||||
CrdsData::SnapshotHashes(hash) => hash.wallclock,
|
||||
CrdsData::AccountsHashes(hash) => hash.wallclock,
|
||||
CrdsData::Version(version) => version.wallclock,
|
||||
}
|
||||
}
|
||||
pub fn pubkey(&self) -> Pubkey {
|
||||
@ -295,6 +328,7 @@ impl CrdsValue {
|
||||
CrdsData::EpochSlots(_, slots) => slots.from,
|
||||
CrdsData::SnapshotHashes(hash) => hash.from,
|
||||
CrdsData::AccountsHashes(hash) => hash.from,
|
||||
CrdsData::Version(version) => version.from,
|
||||
}
|
||||
}
|
||||
pub fn label(&self) -> CrdsValueLabel {
|
||||
@ -304,6 +338,7 @@ impl CrdsValue {
|
||||
CrdsData::EpochSlots(_, _) => CrdsValueLabel::EpochSlots(self.pubkey()),
|
||||
CrdsData::SnapshotHashes(_) => CrdsValueLabel::SnapshotHashes(self.pubkey()),
|
||||
CrdsData::AccountsHashes(_) => CrdsValueLabel::AccountsHashes(self.pubkey()),
|
||||
CrdsData::Version(_) => CrdsValueLabel::Version(self.pubkey()),
|
||||
}
|
||||
}
|
||||
pub fn contact_info(&self) -> Option<&ContactInfo> {
|
||||
@ -347,6 +382,13 @@ impl CrdsValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version(&self) -> Option<&Version> {
|
||||
match &self.data {
|
||||
CrdsData::Version(version) => Some(version),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all the possible labels for a record identified by Pubkey.
|
||||
pub fn record_labels(key: &Pubkey) -> Vec<CrdsValueLabel> {
|
||||
let mut labels = vec![
|
||||
@ -354,6 +396,7 @@ impl CrdsValue {
|
||||
CrdsValueLabel::EpochSlots(*key),
|
||||
CrdsValueLabel::SnapshotHashes(*key),
|
||||
CrdsValueLabel::AccountsHashes(*key),
|
||||
CrdsValueLabel::Version(*key),
|
||||
];
|
||||
labels.extend((0..MAX_VOTES).map(|ix| CrdsValueLabel::Vote(ix, *key)));
|
||||
labels
|
||||
@ -403,7 +446,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_labels() {
|
||||
let mut hits = [false; 4 + MAX_VOTES as usize];
|
||||
let mut hits = [false; 5 + MAX_VOTES as usize];
|
||||
// this method should cover all the possible labels
|
||||
for v in &CrdsValue::record_labels(&Pubkey::default()) {
|
||||
match v {
|
||||
@ -411,7 +454,8 @@ mod test {
|
||||
CrdsValueLabel::EpochSlots(_) => hits[1] = true,
|
||||
CrdsValueLabel::SnapshotHashes(_) => hits[2] = true,
|
||||
CrdsValueLabel::AccountsHashes(_) => hits[3] = true,
|
||||
CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 4] = true,
|
||||
CrdsValueLabel::Version(_) => hits[4] = true,
|
||||
CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 5] = true,
|
||||
}
|
||||
}
|
||||
assert!(hits.iter().all(|x| *x));
|
||||
|
@ -1066,6 +1066,9 @@ impl RpcSol for RpcSolImpl {
|
||||
gossip: Some(contact_info.gossip),
|
||||
tpu: valid_address_or_none(&contact_info.tpu),
|
||||
rpc: valid_address_or_none(&contact_info.rpc),
|
||||
version: cluster_info
|
||||
.get_node_version(&contact_info.id)
|
||||
.map(|v| v.to_string()),
|
||||
})
|
||||
} else {
|
||||
None // Exclude spy nodes
|
||||
@ -1468,7 +1471,7 @@ impl RpcSol for RpcSolImpl {
|
||||
|
||||
fn get_version(&self, _: Self::Metadata) -> Result<RpcVersionInfo> {
|
||||
Ok(RpcVersionInfo {
|
||||
solana_core: solana_clap_utils::version!().to_string(),
|
||||
solana_core: solana_version::Version::default().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -1835,7 +1838,7 @@ pub mod tests {
|
||||
.expect("actual response deserialization");
|
||||
|
||||
let expected = format!(
|
||||
r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}"}}],"id":1}}"#,
|
||||
r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null}}],"id":1}}"#,
|
||||
leader_pubkey,
|
||||
rpc_port::DEFAULT_RPC_PORT
|
||||
);
|
||||
@ -2637,7 +2640,7 @@ pub mod tests {
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"solana-core": solana_clap_utils::version!().to_string()
|
||||
"solana-core": solana_version::version!().to_string()
|
||||
},
|
||||
"id": 1
|
||||
});
|
||||
|
@ -40,7 +40,7 @@ fn test_rpc_client() {
|
||||
|
||||
assert_eq!(
|
||||
client.get_version().unwrap().solana_core,
|
||||
solana_clap_utils::version!()
|
||||
solana_version::version!()
|
||||
);
|
||||
|
||||
assert!(client.get_account(&bob_pubkey).is_err());
|
||||
|
@ -258,7 +258,8 @@ The result field will be an array of JSON objects, each with the following sub f
|
||||
* `pubkey: <string>` - Node public key, as base-58 encoded string
|
||||
* `gossip: <string>` - Gossip network address for the node
|
||||
* `tpu: <string>` - TPU network address for the node
|
||||
* `rpc: <string>` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled
|
||||
* `rpc: <string>|null` - JSON RPC network address for the node, or `null` if the JSON RPC service is not enabled
|
||||
* `version: <string>|null` - The software version of the node, or `null` if the version information is not available
|
||||
|
||||
#### Example:
|
||||
|
||||
@ -267,7 +268,7 @@ The result field will be an array of JSON objects, each with the following sub f
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getClusterNodes"}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","pubkey":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"}],"id":1}
|
||||
{"jsonrpc":"2.0","result":[{"gossip":"10.239.6.48:8001","pubkey":"9QzsJf7LPLj8GkXbYT3LFDKqsj2hHG7TA3xinJHu8epQ","rpc":"10.239.6.48:8899","tpu":"10.239.6.48:8856"},"version":"1.0.0 c375ce1f"],"id":1}
|
||||
```
|
||||
|
||||
### getConfirmedBlock
|
||||
|
@ -27,6 +27,7 @@ use solana_ledger::bank_forks::SnapshotConfig;
|
||||
use solana_perf::recycler::enable_recycler_warming;
|
||||
use solana_sdk::{
|
||||
clock::Slot,
|
||||
commitment_config::CommitmentConfig,
|
||||
genesis_config::GenesisConfig,
|
||||
hash::Hash,
|
||||
pubkey::Pubkey,
|
||||
@ -398,8 +399,10 @@ fn check_vote_account(
|
||||
node_pubkey: &Pubkey,
|
||||
) -> Result<(), String> {
|
||||
let found_vote_account = rpc_client
|
||||
.get_account(vote_pubkey)
|
||||
.map_err(|err| format!("Failed to get vote account: {}", err.to_string()))?;
|
||||
.get_account_with_commitment(vote_pubkey, CommitmentConfig::root())
|
||||
.map_err(|err| format!("Failed to get vote account: {}", err.to_string()))?
|
||||
.value
|
||||
.ok_or_else(|| format!("vote account does not exist: {}", vote_pubkey))?;
|
||||
|
||||
if found_vote_account.owner != solana_vote_program::id() {
|
||||
return Err(format!(
|
||||
@ -409,8 +412,10 @@ fn check_vote_account(
|
||||
}
|
||||
|
||||
let found_node_account = rpc_client
|
||||
.get_account(node_pubkey)
|
||||
.map_err(|err| format!("Failed to get identity account: {}", err.to_string()))?;
|
||||
.get_account_with_commitment(node_pubkey, CommitmentConfig::root())
|
||||
.map_err(|err| format!("Failed to get identity account: {}", err.to_string()))?
|
||||
.value
|
||||
.ok_or_else(|| format!("identity account does not exist: {}", node_pubkey))?;
|
||||
|
||||
let found_vote_account = solana_vote_program::vote_state::VoteState::from(&found_vote_account);
|
||||
if let Some(found_vote_account) = found_vote_account {
|
||||
@ -1282,7 +1287,7 @@ pub fn main() {
|
||||
})
|
||||
.and_then(|_| {
|
||||
if let Some(snapshot_hash) = snapshot_hash {
|
||||
rpc_client.get_slot()
|
||||
rpc_client.get_slot_with_commitment(CommitmentConfig::root())
|
||||
.map_err(|err| format!("Failed to get RPC node slot: {}", err))
|
||||
.and_then(|slot| {
|
||||
info!("RPC node root slot: {}", slot);
|
||||
|
2
version/.gitignore
vendored
Normal file
2
version/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target/
|
||||
/farf/
|
20
version/Cargo.toml
Normal file
20
version/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "solana-version"
|
||||
version = "1.1.11"
|
||||
description = "Solana Version"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.105"
|
||||
serde_derive = "1.0.103"
|
||||
solana-sdk = { path = "../sdk", version = "1.0.23" }
|
||||
|
||||
[lib]
|
||||
name = "solana_version"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
70
version/src/lib.rs
Normal file
70
version/src/lib.rs
Normal file
@ -0,0 +1,70 @@
|
||||
extern crate serde_derive;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::sanitize::Sanitize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct Version {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
patch: u16,
|
||||
commit: Option<u32>, // first 4 bytes of the sha1 commit hash
|
||||
}
|
||||
|
||||
fn compute_commit(sha1: Option<&'static str>) -> Option<u32> {
|
||||
let sha1 = sha1?;
|
||||
if sha1.len() < 8 {
|
||||
None
|
||||
} else {
|
||||
u32::from_str_radix(&sha1[..8], 16).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Version {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
|
||||
minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
|
||||
patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
|
||||
commit: compute_commit(option_env!("CI_COMMIT")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}.{}.{} {}",
|
||||
self.major,
|
||||
self.minor,
|
||||
self.patch,
|
||||
match self.commit {
|
||||
None => "devbuild".to_string(),
|
||||
Some(commit) => format!("{:08x}", commit),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sanitize for Version {}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! version {
|
||||
() => {
|
||||
&*format!("{}", $crate::Version::default())
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compute_commit() {
|
||||
assert_eq!(compute_commit(None), None);
|
||||
assert_eq!(compute_commit(Some("1234567890")), Some(0x12345678));
|
||||
assert_eq!(compute_commit(Some("HEAD")), None);
|
||||
assert_eq!(compute_commit(Some("garbagein")), None);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user