ok/jj
1
0
Fork 0
forked from mirrors/jj
jj/lib/src/proto_op_store.rs
Martin von Zweigbergk c269b72fb3 op_store: auto-upgrade existing repos from Thrift to Protobuf
With this patch, we auto-upgrade existing repos that use Thrift format
for the operation log to use Protobuf format. That would only be repos
used with an unreleased version of jj after 0.5.1 (which may be the
majority of repos?).

The upgrade from Thrift is simpler because we now use the same hashing
scheme for the Protobuf-based storage, so the operation and view IDs
remain the same as they were in the Thrift-based storage. We could
simplify the code a bit more as a result, but since this code is
supposed to be short-lived, I didn't bother.

Since the change from the Protobuf format with the old hashing scheme
to a the (same) Protobuf format with the new hashing scheme shouldn't
impact users, I removed the entry we had in the changelog about the
format change.
2022-12-03 22:31:02 -08:00

346 lines
11 KiB
Rust

// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::fs;
use std::fs::File;
use std::io::ErrorKind;
use std::path::PathBuf;
use itertools::Itertools;
use protobuf::{Message, MessageField};
use tempfile::NamedTempFile;
use crate::backend::{CommitId, MillisSinceEpoch, Timestamp};
use crate::content_hash::blake2b_hash;
use crate::file_util::persist_content_addressed_temp_file;
use crate::op_store::{
BranchTarget, OpStore, OpStoreError, OpStoreResult, Operation, OperationId, OperationMetadata,
RefTarget, View, ViewId, WorkspaceId,
};
impl From<protobuf::Error> for OpStoreError {
fn from(err: protobuf::Error) -> Self {
OpStoreError::Other(err.to_string())
}
}
#[derive(Debug)]
pub struct ProtoOpStore {
path: PathBuf,
}
impl ProtoOpStore {
pub fn init(store_path: PathBuf) -> Self {
fs::create_dir(store_path.join("views")).unwrap();
fs::create_dir(store_path.join("operations")).unwrap();
ProtoOpStore { path: store_path }
}
pub fn load(store_path: PathBuf) -> Self {
ProtoOpStore { path: store_path }
}
fn view_path(&self, id: &ViewId) -> PathBuf {
self.path.join("views").join(id.hex())
}
fn operation_path(&self, id: &OperationId) -> PathBuf {
self.path.join("operations").join(id.hex())
}
}
fn not_found_to_store_error(err: std::io::Error) -> OpStoreError {
if err.kind() == ErrorKind::NotFound {
OpStoreError::NotFound
} else {
OpStoreError::from(err)
}
}
impl OpStore for ProtoOpStore {
fn read_view(&self, id: &ViewId) -> OpStoreResult<View> {
let path = self.view_path(id);
let mut file = File::open(path).map_err(not_found_to_store_error)?;
let proto: crate::protos::op_store::View = Message::parse_from_reader(&mut file)?;
Ok(view_from_proto(&proto))
}
fn write_view(&self, view: &View) -> OpStoreResult<ViewId> {
let temp_file = NamedTempFile::new_in(&self.path)?;
let proto = view_to_proto(view);
proto.write_to_writer(&mut temp_file.as_file())?;
let id = ViewId::new(blake2b_hash(view).to_vec());
persist_content_addressed_temp_file(temp_file, self.view_path(&id))?;
Ok(id)
}
fn read_operation(&self, id: &OperationId) -> OpStoreResult<Operation> {
let path = self.operation_path(id);
let mut file = File::open(path).map_err(not_found_to_store_error)?;
let proto: crate::protos::op_store::Operation = Message::parse_from_reader(&mut file)?;
Ok(operation_from_proto(&proto))
}
fn write_operation(&self, operation: &Operation) -> OpStoreResult<OperationId> {
let temp_file = NamedTempFile::new_in(&self.path)?;
let proto = operation_to_proto(operation);
proto.write_to_writer(&mut temp_file.as_file())?;
let id = OperationId::new(blake2b_hash(operation).to_vec());
persist_content_addressed_temp_file(temp_file, self.operation_path(&id))?;
Ok(id)
}
}
fn timestamp_to_proto(timestamp: &Timestamp) -> crate::protos::op_store::Timestamp {
let mut proto = crate::protos::op_store::Timestamp::new();
proto.millis_since_epoch = timestamp.timestamp.0;
proto.tz_offset = timestamp.tz_offset;
proto
}
fn timestamp_from_proto(proto: &crate::protos::op_store::Timestamp) -> Timestamp {
Timestamp {
timestamp: MillisSinceEpoch(proto.millis_since_epoch),
tz_offset: proto.tz_offset,
}
}
fn operation_metadata_to_proto(
metadata: &OperationMetadata,
) -> crate::protos::op_store::OperationMetadata {
let mut proto = crate::protos::op_store::OperationMetadata::new();
proto.start_time = MessageField::some(timestamp_to_proto(&metadata.start_time));
proto.end_time = MessageField::some(timestamp_to_proto(&metadata.end_time));
proto.description = metadata.description.clone();
proto.hostname = metadata.hostname.clone();
proto.username = metadata.username.clone();
proto.tags = metadata.tags.clone();
proto
}
fn operation_metadata_from_proto(
proto: &crate::protos::op_store::OperationMetadata,
) -> OperationMetadata {
let start_time = timestamp_from_proto(&proto.start_time);
let end_time = timestamp_from_proto(&proto.end_time);
let description = proto.description.to_owned();
let hostname = proto.hostname.to_owned();
let username = proto.username.to_owned();
let tags = proto.tags.clone();
OperationMetadata {
start_time,
end_time,
description,
hostname,
username,
tags,
}
}
fn operation_to_proto(operation: &Operation) -> crate::protos::op_store::Operation {
let mut proto = crate::protos::op_store::Operation::new();
proto.view_id = operation.view_id.as_bytes().to_vec();
for parent in &operation.parents {
proto.parents.push(parent.to_bytes());
}
proto.metadata = MessageField::some(operation_metadata_to_proto(&operation.metadata));
proto
}
fn operation_from_proto(proto: &crate::protos::op_store::Operation) -> Operation {
let operation_id_from_proto = |parent: &Vec<u8>| OperationId::new(parent.clone());
let parents = proto.parents.iter().map(operation_id_from_proto).collect();
let view_id = ViewId::new(proto.view_id.clone());
let metadata = operation_metadata_from_proto(&proto.metadata);
Operation {
view_id,
parents,
metadata,
}
}
fn view_to_proto(view: &View) -> crate::protos::op_store::View {
let mut proto = crate::protos::op_store::View::new();
for (workspace_id, commit_id) in &view.wc_commit_ids {
proto
.wc_commit_ids
.insert(workspace_id.as_str().to_string(), commit_id.to_bytes());
}
for head_id in &view.head_ids {
proto.head_ids.push(head_id.to_bytes());
}
for head_id in &view.public_head_ids {
proto.public_head_ids.push(head_id.to_bytes());
}
for (name, target) in &view.branches {
let mut branch_proto = crate::protos::op_store::Branch::new();
branch_proto.name = name.clone();
if let Some(local_target) = &target.local_target {
branch_proto.local_target = MessageField::some(ref_target_to_proto(local_target));
}
for (remote_name, target) in &target.remote_targets {
let mut remote_branch_proto = crate::protos::op_store::RemoteBranch::new();
remote_branch_proto.remote_name = remote_name.clone();
remote_branch_proto.target = MessageField::some(ref_target_to_proto(target));
branch_proto.remote_branches.push(remote_branch_proto);
}
proto.branches.push(branch_proto);
}
for (name, target) in &view.tags {
let mut tag_proto = crate::protos::op_store::Tag::new();
tag_proto.name = name.clone();
tag_proto.target = MessageField::some(ref_target_to_proto(target));
proto.tags.push(tag_proto);
}
for (git_ref_name, target) in &view.git_refs {
let mut git_ref_proto = crate::protos::op_store::GitRef::new();
git_ref_proto.name = git_ref_name.clone();
git_ref_proto.target = MessageField::some(ref_target_to_proto(target));
proto.git_refs.push(git_ref_proto);
}
if let Some(git_head) = &view.git_head {
proto.git_head = git_head.to_bytes();
}
proto
}
fn view_from_proto(proto: &crate::protos::op_store::View) -> View {
let mut view = View::default();
// For compatibility with old repos before we had support for multiple working
// copies
if !proto.wc_commit_id.is_empty() {
view.wc_commit_ids.insert(
WorkspaceId::default(),
CommitId::new(proto.wc_commit_id.clone()),
);
}
for (workspace_id, commit_id) in &proto.wc_commit_ids {
view.wc_commit_ids.insert(
WorkspaceId::new(workspace_id.clone()),
CommitId::new(commit_id.clone()),
);
}
for head_id_bytes in &proto.head_ids {
view.head_ids.insert(CommitId::from_bytes(head_id_bytes));
}
for head_id_bytes in &proto.public_head_ids {
view.public_head_ids
.insert(CommitId::from_bytes(head_id_bytes));
}
for branch_proto in &proto.branches {
let local_target = branch_proto
.local_target
.as_ref()
.map(ref_target_from_proto);
let mut remote_targets = BTreeMap::new();
for remote_branch in &branch_proto.remote_branches {
remote_targets.insert(
remote_branch.remote_name.clone(),
ref_target_from_proto(&remote_branch.target),
);
}
view.branches.insert(
branch_proto.name.clone(),
BranchTarget {
local_target,
remote_targets,
},
);
}
for tag_proto in &proto.tags {
view.tags.insert(
tag_proto.name.clone(),
ref_target_from_proto(&tag_proto.target),
);
}
for git_ref in &proto.git_refs {
if let Some(target) = git_ref.target.as_ref() {
view.git_refs
.insert(git_ref.name.clone(), ref_target_from_proto(target));
} else {
// Legacy format
view.git_refs.insert(
git_ref.name.clone(),
RefTarget::Normal(CommitId::new(git_ref.commit_id.clone())),
);
}
}
if !proto.git_head.is_empty() {
view.git_head = Some(CommitId::new(proto.git_head.clone()));
}
view
}
fn ref_target_to_proto(value: &RefTarget) -> crate::protos::op_store::RefTarget {
let mut proto = crate::protos::op_store::RefTarget::new();
match value {
RefTarget::Normal(id) => {
proto.set_commit_id(id.to_bytes());
}
RefTarget::Conflict { removes, adds } => {
let mut ref_conflict_proto = crate::protos::op_store::RefConflict::new();
for id in removes {
ref_conflict_proto.removes.push(id.to_bytes());
}
for id in adds {
ref_conflict_proto.adds.push(id.to_bytes());
}
proto.set_conflict(ref_conflict_proto);
}
}
proto
}
fn ref_target_from_proto(proto: &crate::protos::op_store::RefTarget) -> RefTarget {
match proto.value.as_ref().unwrap() {
crate::protos::op_store::ref_target::Value::CommitId(id) => {
RefTarget::Normal(CommitId::from_bytes(id))
}
crate::protos::op_store::ref_target::Value::Conflict(conflict) => {
let removes = conflict
.removes
.iter()
.map(|id_bytes| CommitId::from_bytes(id_bytes))
.collect_vec();
let adds = conflict
.adds
.iter()
.map(|id_bytes| CommitId::from_bytes(id_bytes))
.collect_vec();
RefTarget::Conflict { removes, adds }
}
}
}