mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-29 12:33:49 +03:00
use reflog for session activity
This commit is contained in:
parent
4c5f52ee6d
commit
35e2059a2f
@ -194,31 +194,32 @@ pub fn get_current_file_deltas(
|
||||
file_path: &Path,
|
||||
) -> Result<Option<Vec<Delta>>, Error> {
|
||||
let deltas_path = project_path.join(".git/gb/session/deltas");
|
||||
if !deltas_path.exists() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let file_deltas_path = deltas_path.join(file_path);
|
||||
let file_deltas = std::fs::read_to_string(&file_deltas_path).map_err(|e| Error {
|
||||
message: format!(
|
||||
"Could not read delta file at {}",
|
||||
file_deltas_path.display()
|
||||
),
|
||||
cause: e.into(),
|
||||
});
|
||||
match file_deltas {
|
||||
Ok(file_deltas) => {
|
||||
let file_deltas: Vec<Delta> =
|
||||
serde_json::from_str(&file_deltas).map_err(|e| Error {
|
||||
message: format!(
|
||||
"Could not parse delta file at {}",
|
||||
file_deltas_path.display()
|
||||
),
|
||||
cause: e.into(),
|
||||
})?;
|
||||
Ok(Some(file_deltas))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
let file_deltas_path = deltas_path.join(file_path);
|
||||
if !file_deltas_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file_deltas = std::fs::read_to_string(&file_deltas_path).map_err(|e| Error {
|
||||
message: format!(
|
||||
"Could not read delta file at {}",
|
||||
file_deltas_path.display()
|
||||
),
|
||||
cause: e.into(),
|
||||
});
|
||||
|
||||
match file_deltas {
|
||||
Ok(file_deltas) => {
|
||||
let file_deltas: Vec<Delta> =
|
||||
serde_json::from_str(&file_deltas).map_err(|e| Error {
|
||||
message: format!(
|
||||
"Could not parse delta file at {}",
|
||||
file_deltas_path.display()
|
||||
),
|
||||
cause: e.into(),
|
||||
})?;
|
||||
Ok(Some(file_deltas))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,130 @@ pub struct Session {
|
||||
// if hash is not set, the session is not saved aka current
|
||||
pub hash: Option<String>,
|
||||
pub meta: Meta,
|
||||
pub activity: Vec<Activity>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ActivityType {
|
||||
Commit,
|
||||
Checkout,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Activity {
|
||||
#[serde(rename = "type")]
|
||||
pub activity_type: ActivityType,
|
||||
pub timestamp: u64,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
fn parse_reflog_line(line: &str) -> Result<Option<Activity>, Error> {
|
||||
match line.split("\t").collect::<Vec<&str>>()[..] {
|
||||
[meta, message] => {
|
||||
let meta_parts = meta.split_whitespace().collect::<Vec<&str>>();
|
||||
let timestamp = meta_parts[meta_parts.len() - 2]
|
||||
.parse::<u64>()
|
||||
.map_err(|err| Error {
|
||||
cause: ErrorCause::ParseIntError(err),
|
||||
message: "Error while parsing reflog timestamp".to_string(),
|
||||
})?;
|
||||
|
||||
match message.split(": ").collect::<Vec<&str>>()[..] {
|
||||
[entry_type, msg] => match entry_type {
|
||||
"commit" => Ok(Some(Activity {
|
||||
activity_type: ActivityType::Commit,
|
||||
message: msg.to_string(),
|
||||
timestamp,
|
||||
})),
|
||||
"checkout" => Ok(Some(Activity {
|
||||
activity_type: ActivityType::Checkout,
|
||||
message: msg.to_string(),
|
||||
timestamp,
|
||||
})),
|
||||
_ => Ok(None),
|
||||
},
|
||||
_ => Err(Error {
|
||||
cause: ErrorCause::ParseActivityError,
|
||||
message: "Error parsing reflog activity message".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
_ => Err(Error {
|
||||
cause: ErrorCause::ParseActivityError,
|
||||
message: "Error while parsing reflog activity".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn current(repo: &git2::Repository) -> Result<Option<Self>, Error> {
|
||||
let session_path = repo.path().join("gb/session");
|
||||
if !session_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let meta_path = session_path.join("meta");
|
||||
|
||||
let start_path = meta_path.join("start");
|
||||
let start_ts = std::fs::read_to_string(start_path)
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read session start".to_string(),
|
||||
})?
|
||||
.parse::<u64>()
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to parse session start".to_string(),
|
||||
})?;
|
||||
|
||||
let last_path = meta_path.join("last");
|
||||
let last_ts = std::fs::read_to_string(last_path)
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read session last".to_string(),
|
||||
})?
|
||||
.parse::<u64>()
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to parse session last".to_string(),
|
||||
})?;
|
||||
|
||||
let branch_path = meta_path.join("branch");
|
||||
let branch = std::fs::read_to_string(branch_path).map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read branch".to_string(),
|
||||
})?;
|
||||
|
||||
let commit_path = meta_path.join("commit");
|
||||
let commit = std::fs::read_to_string(commit_path).map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read commit".to_string(),
|
||||
})?;
|
||||
|
||||
let reflog = std::fs::read_to_string(repo.path().join("logs/HEAD")).map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read reflog".to_string(),
|
||||
})?;
|
||||
let activity = reflog
|
||||
.lines()
|
||||
.filter_map(|line| parse_reflog_line(line).unwrap_or(None))
|
||||
.filter(|activity| activity.timestamp >= start_ts)
|
||||
.collect::<Vec<Activity>>();
|
||||
|
||||
Ok(Some(Session {
|
||||
hash: None,
|
||||
activity,
|
||||
meta: Meta {
|
||||
start_ts,
|
||||
last_ts,
|
||||
branch,
|
||||
commit,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_commit(repo: &git2::Repository, commit: &git2::Commit) -> Result<Self, Error> {
|
||||
let tree = commit.tree().map_err(|err| Error {
|
||||
cause: err.into(),
|
||||
@ -44,6 +165,13 @@ impl Session {
|
||||
message: "Error while parsing last file".to_string(),
|
||||
})?;
|
||||
|
||||
let reflog = read_as_string(repo, &tree, Path::new("logs/HEAD"))?;
|
||||
let activity = reflog
|
||||
.lines()
|
||||
.filter_map(|line| parse_reflog_line(line).unwrap_or(None))
|
||||
.filter(|activity| activity.timestamp >= start)
|
||||
.collect::<Vec<Activity>>();
|
||||
|
||||
Ok(Session {
|
||||
hash: Some(commit.id().to_string()),
|
||||
meta: Meta {
|
||||
@ -52,6 +180,7 @@ impl Session {
|
||||
branch: read_as_string(repo, &tree, Path::new("session/meta/branch"))?,
|
||||
commit: read_as_string(repo, &tree, Path::new("session/meta/commit"))?,
|
||||
},
|
||||
activity,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -71,6 +200,7 @@ impl std::error::Error for Error {
|
||||
ErrorCause::SessionDoesNotExistError => Some(self),
|
||||
ErrorCause::GitError(err) => Some(err),
|
||||
ErrorCause::ParseUtf8Error(err) => Some(err),
|
||||
ErrorCause::ParseActivityError => Some(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,6 +214,7 @@ impl std::fmt::Display for Error {
|
||||
ErrorCause::SessionDoesNotExistError => write!(f, "{}", self.message),
|
||||
ErrorCause::GitError(ref e) => write!(f, "{}: {}", self.message, e),
|
||||
ErrorCause::ParseUtf8Error(ref e) => write!(f, "{}: {}", self.message, e),
|
||||
ErrorCause::ParseActivityError => write!(f, "{}", self.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,6 +227,7 @@ pub enum ErrorCause {
|
||||
SessionExistsError,
|
||||
SessionDoesNotExistError,
|
||||
ParseUtf8Error(std::string::FromUtf8Error),
|
||||
ParseActivityError,
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for ErrorCause {
|
||||
@ -192,61 +324,6 @@ pub fn delete_current_session(repo: &git2::Repository) -> Result<(), std::io::Er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current_session(repo: &git2::Repository) -> Result<Option<Session>, Error> {
|
||||
let session_path = repo.path().join("gb/session");
|
||||
if !session_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let meta_path = session_path.join("meta");
|
||||
|
||||
let start_path = meta_path.join("start");
|
||||
let start_ts = std::fs::read_to_string(start_path)
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read session start".to_string(),
|
||||
})?
|
||||
.parse::<u64>()
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to parse session start".to_string(),
|
||||
})?;
|
||||
|
||||
let last_path = meta_path.join("last");
|
||||
let last_ts = std::fs::read_to_string(last_path)
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read session last".to_string(),
|
||||
})?
|
||||
.parse::<u64>()
|
||||
.map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to parse session last".to_string(),
|
||||
})?;
|
||||
|
||||
let branch_path = meta_path.join("branch");
|
||||
let branch = std::fs::read_to_string(branch_path).map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read branch".to_string(),
|
||||
})?;
|
||||
|
||||
let commit_path = meta_path.join("commit");
|
||||
let commit = std::fs::read_to_string(commit_path).map_err(|e| Error {
|
||||
cause: e.into(),
|
||||
message: "failed to read commit".to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Some(Session {
|
||||
hash: None,
|
||||
meta: Meta {
|
||||
start_ts,
|
||||
last_ts,
|
||||
branch,
|
||||
commit,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn list_sessions(repo: &git2::Repository) -> Result<Vec<Session>, Error> {
|
||||
match repo.revparse_single("refs/gitbutler/current") {
|
||||
Err(_) => Ok(vec![]),
|
||||
|
@ -232,7 +232,7 @@ fn write_beginning_meta_files(repo: &Repository) -> Result<(), Box<dyn std::erro
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
match sessions::get_current_session(repo)
|
||||
match sessions::Session::current(repo)
|
||||
.map_err(|e| format!("Error while getting current session: {}", e.to_string()))?
|
||||
{
|
||||
Some(mut session) => {
|
||||
@ -251,6 +251,7 @@ fn write_beginning_meta_files(repo: &Repository) -> Result<(), Box<dyn std::erro
|
||||
branch: head.name().unwrap().to_string(),
|
||||
commit: head.peel_to_commit()?.id().to_string(),
|
||||
},
|
||||
activity: vec![],
|
||||
};
|
||||
sessions::create_current_session(repo, &session)
|
||||
.map_err(|e| format!("Error while creating current session: {}", e.to_string()))?;
|
||||
|
@ -81,10 +81,26 @@ fn check_for_changes(
|
||||
repo: &Repository,
|
||||
) -> Result<Option<sessions::Session>, Box<dyn std::error::Error>> {
|
||||
if ready_to_commit(repo)? {
|
||||
let tree = build_initial_wd_tree(&repo)?;
|
||||
let gb_tree = build_gb_tree(tree, &repo)?;
|
||||
let wd_index = &mut git2::Index::new()?;
|
||||
build_wd_index(&repo, wd_index)?;
|
||||
let wd_tree = wd_index.write_tree_to(&repo)?;
|
||||
|
||||
let commit_oid = write_gb_commit(gb_tree, &repo)?;
|
||||
let session_index = &mut git2::Index::new()?;
|
||||
build_session_index(&repo, session_index)?;
|
||||
let session_tree = session_index.write_tree_to(&repo)?;
|
||||
|
||||
let log_index = &mut git2::Index::new()?;
|
||||
build_log_index(&repo, log_index)?;
|
||||
let log_tree = log_index.write_tree_to(&repo)?;
|
||||
|
||||
let mut tree_builder = repo.treebuilder(None)?;
|
||||
tree_builder.insert("session", session_tree, 0o040000)?;
|
||||
tree_builder.insert("wd", wd_tree, 0o040000)?;
|
||||
tree_builder.insert("logs", log_tree, 0o040000)?;
|
||||
|
||||
let tree = tree_builder.write()?;
|
||||
|
||||
let commit_oid = write_gb_commit(tree, &repo)?;
|
||||
log::debug!(
|
||||
"{}: wrote gb commit {}",
|
||||
repo.workdir().unwrap().display(),
|
||||
@ -108,7 +124,7 @@ fn check_for_changes(
|
||||
// and that there has been no activity in the last 5 minutes (the session appears to be over)
|
||||
// and the start was at most an hour ago
|
||||
fn ready_to_commit(repo: &Repository) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
if let Some(current_session) = sessions::get_current_session(repo)? {
|
||||
if let Some(current_session) = sessions::Session::current(repo)? {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@ -141,9 +157,11 @@ fn ready_to_commit(repo: &Repository) -> Result<bool, Box<dyn std::error::Error>
|
||||
// build the initial tree from the working directory, not taking into account the gitbutler metadata
|
||||
// eventually we might just want to run this once and then update it with the files that are changed over time, but right now we're running it every commit
|
||||
// it ignores files that are in the .gitignore
|
||||
fn build_initial_wd_tree(repo: &Repository) -> Result<git2::Oid, Box<dyn std::error::Error>> {
|
||||
fn build_wd_index(
|
||||
repo: &Repository,
|
||||
index: &mut git2::Index,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// create a new in-memory git2 index and open the working one so we can cheat if none of the metadata of an entry has changed
|
||||
let wd_index = &mut git2::Index::new()?;
|
||||
let repo_index = &mut repo.index()?;
|
||||
|
||||
// add all files in the working directory to the in-memory index, skipping for matching entries in the repo index
|
||||
@ -151,13 +169,11 @@ fn build_initial_wd_tree(repo: &Repository) -> Result<git2::Oid, Box<dyn std::er
|
||||
for file in all_files {
|
||||
let file_path = Path::new(&file);
|
||||
if !repo.is_path_ignored(&file).unwrap_or(true) {
|
||||
add_path(wd_index, repo_index, &file_path, &repo)?;
|
||||
add_path(index, repo_index, &file_path, &repo)?;
|
||||
}
|
||||
}
|
||||
|
||||
// write the in-memory index to the repo
|
||||
let tree = wd_index.write_tree_to(&repo)?;
|
||||
Ok(tree)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// take a file path we see and add it to our in-memory index
|
||||
@ -275,41 +291,47 @@ fn sha256_digest(path: &Path) -> Result<String, std::io::Error> {
|
||||
Ok(format!("{:X}", digest))
|
||||
}
|
||||
|
||||
// this builds the tree that we're going to link to from our commit.
|
||||
// it has two entries, wd and session:
|
||||
// - wd: the tree that is the working directory recorded at the end of the session
|
||||
// - session/deltas: the tree that contains the crdt data for each file that changed during the session
|
||||
// - session/meta: some metadata values like starting time, last touched time, branch, etc
|
||||
// returns a tree Oid that can be used to create a commit
|
||||
fn build_gb_tree(
|
||||
tree: git2::Oid,
|
||||
fn build_log_index(
|
||||
repo: &Repository,
|
||||
) -> Result<git2::Oid, Box<dyn std::error::Error>> {
|
||||
// create a new, awesome tree with TreeBuilder
|
||||
let mut tree_builder = repo.treebuilder(None)?;
|
||||
index: &mut git2::Index,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let log_path = repo.path().join("logs/HEAD");
|
||||
log::debug!("Adding log path: {}", log_path.display());
|
||||
|
||||
// insert the tree oid as a subdirectory under the name 'wd'
|
||||
tree_builder.insert("wd", tree, 0o040000)?;
|
||||
let metadata = log_path.metadata()?;
|
||||
let mtime = FileTime::from_last_modification_time(&metadata);
|
||||
let ctime = FileTime::from_creation_time(&metadata).unwrap();
|
||||
|
||||
// create a new in-memory git2 index and fill it with the contents of .git/gb/session
|
||||
let session_index = &mut git2::Index::new()?;
|
||||
index.add(&git2::IndexEntry {
|
||||
ctime: IndexTime::new(ctime.seconds().try_into()?, ctime.nanoseconds().try_into()?),
|
||||
mtime: IndexTime::new(mtime.seconds().try_into()?, mtime.nanoseconds().try_into()?),
|
||||
dev: metadata.dev().try_into()?,
|
||||
ino: metadata.ino().try_into()?,
|
||||
mode: metadata.mode(),
|
||||
uid: metadata.uid().try_into()?,
|
||||
gid: metadata.gid().try_into()?,
|
||||
file_size: metadata.len().try_into()?,
|
||||
flags: 10, // normal flags for normal file (for the curious: https://git-scm.com/docs/index-format)
|
||||
flags_extended: 0, // no extended flags
|
||||
path: "HEAD".to_string().into(),
|
||||
id: repo.blob_path(&log_path)?,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_session_index(
|
||||
repo: &Repository,
|
||||
index: &mut git2::Index,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// add all files in the working directory to the in-memory index, skipping for matching entries in the repo index
|
||||
let session_dir = repo.path().join("gb/session");
|
||||
for session_file in fs::list_files(&session_dir)? {
|
||||
let file_path = Path::new(&session_file);
|
||||
add_session_path(&repo, session_index, &file_path)?;
|
||||
add_session_path(&repo, index, &file_path)?;
|
||||
}
|
||||
|
||||
// write the in-memory index to the repo
|
||||
let session_tree = session_index.write_tree_to(&repo)?;
|
||||
|
||||
// insert the session tree oid as a subdirectory under the name 'session'
|
||||
tree_builder.insert("session", session_tree, 0o040000)?;
|
||||
|
||||
// write the new tree and return the Oid
|
||||
let tree = tree_builder.write().unwrap();
|
||||
Ok(tree)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this is a helper function for build_gb_tree that takes paths under .git/gb/session and adds them to the in-memory index
|
||||
@ -323,7 +345,7 @@ fn add_session_path(
|
||||
log::debug!("Adding session path: {}", file_path.display());
|
||||
|
||||
let blob = repo.blob_path(&file_path)?;
|
||||
let metadata = file_path.metadata().unwrap();
|
||||
let metadata = file_path.metadata()?;
|
||||
let mtime = FileTime::from_last_modification_time(&metadata);
|
||||
let ctime = FileTime::from_creation_time(&metadata).unwrap();
|
||||
|
||||
|
@ -2,6 +2,12 @@ import { invoke } from "@tauri-apps/api";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type Activity = {
|
||||
type: "commit" | "checkout";
|
||||
timestamp: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
hash: string;
|
||||
meta: {
|
||||
@ -10,6 +16,7 @@ export type Session = {
|
||||
branch: string;
|
||||
commit: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
};
|
||||
|
||||
export const listFiles = (params: { projectId: string; sessionId: string }) =>
|
||||
@ -24,8 +31,6 @@ export default async (params: { projectId: string }) => {
|
||||
const eventName = `project://${params.projectId}/sessions`;
|
||||
|
||||
await appWindow.listen<Session>(eventName, (event) => {
|
||||
console.log("HERE")
|
||||
console.log(event)
|
||||
store.update((sessions) => [...sessions, event.payload]);
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user