Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions aria/contents/docs/libra/command/reflog/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: The reflog Command
description: Manage reflog information
---

### Usage

`libra reflog <SUBCOMMAND> [OPTIONS]`

### Subcommands

- `show`
Show the reflog history. This is the default action if no subcommand is provided.

- `delete`
Delete specific entries from the reflog.

- `exists`
Check if a ref has a reflog.

### Description

The libra reflog command is used to manage the log of reference changes. It tracks when the tips of branches and other references were updated in the local repository. It's a powerful tool for recovering lost work and undoing mistakes, acting as a "safety net" for your repository.

- To show the history of HEAD, simply run: libra reflog or libra reflog show

- To show the history of a specific branch: libra reflog show `<branch-name>`

- To delete the second-to-last entry of the HEAD reflog: libra reflog delete HEAD@{1}

### Options (reflog show)

- `<ref>`
The reference to show the log for (e.g., HEAD, main, origin/main). Defaults to HEAD.

- `--pretty=<format>`
Format the output of the reflog entries. Supported formats are:

oneline: The default, compact format (`<hash> <selector>: <subject>`).

short: Shows commit hash, author, and subject.

medium: Shows commit hash, author, date, and full message.

full: Shows author and committer information.

- `-h`, `--help`
Print help for the reflog command or a specific subcommand.

### Options (reflog delete)

`<entry>...`
One or more reflog entries to delete (e.g., HEAD@{2}, main@{5}). This argument is required.

<Note type="note" title="Note">
The `reflog` command is indispensable for recovering from situations where you might think you've lost work, such as
after a faulty `reset` or `rebase`. It tracks local movements of references, so
it won't be available in bare or freshly cloned repositories until some actions are performed.
Refs https://git-scm.com/docs/git-reflog for more information.
</Note>

28 changes: 26 additions & 2 deletions aria/contents/docs/libra/internal/scheme/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description:

Libra use `sea-orm` to interact with sqlite database. The data model is defined in [`libra/src/internal/model`](https://github.com/web3infra-foundation/mega/tree/main/libra/src/internal/model), with two tables: `config` and `reference`.

The `config` table is used to store the configuration of the project, which corresponds to the `config` file in git. The `reference` table is used to store the reference of the project, which corresponds to the `HEAD` and `refs/*` files in git.
The `config` table is used to store the configuration of the project, which corresponds to the `config` file in git. The `reference` table is used to store the reference of the project, which corresponds to the `HEAD` and `refs/*` files in git. This `reflog` is used to record the history of changes to references, corresponding to the files in the `.git/logs/` directory in git.

The relationship between the git file and the sqlite table is as follows:

Expand Down Expand Up @@ -36,11 +36,19 @@ The relationship between the git file and the sqlite table is as follows:
| ------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.gitconfig` | Ini format configuration | Config(configuration=”core”; name=null; key=”filemode”;value=”true” )<br /><br /> Config(configuration=”core”; name=null; key=”ignorecase”;value=”false” ) <br /><br /> Config(configuration=”remote”; name=”origin”; key=”url”;value=”url.git” )<br /><br /> Config(configuration=”remote”; name=”null”; key=”fetch”;value=”+refs……” ) |

#### reflog:

The `reflog` table records every movement of the `HEAD` and branch references. Whenever a reference (like `HEAD` or `refs/heads/main`) is updated, a new record is inserted into this table to describe the change.

| Category | Description | Database format |
| --------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.git/logs/HEAD`<br/>`.git/logs/refs/heads/<branch>` | Records the history of reference changes | Reflog(<br/> ref_name=`<ref_name>`,<br/> old_oid=`<previous_hash>`,<br/> new_oid=`<new_hash>`,<br/> committer_name=`<name>`,<br/> committer_email=`<email>`,<br/> timestamp=`<unix_timestamp>`,<br/> action=`<action_type>`,<br/> message=`<description>`<br/>) |

### Business Model Design

For decoupling, the `sea-orm` model is not directly used in the code, and the business model is redefined (located in `libra/src/internal`), and common CRUD operations are implemented.

Currently, the `config`, `head` and `reference` models are implemented.
Currently, the `config`, `reference` and `reflog` models are implemented.

### SQL Statement

Expand All @@ -52,6 +60,7 @@ CREATE TABLE IF NOT EXISTS `config` (
`key` TEXT NOT NULL,
`value` TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS `reference` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
-- name can't be ''
Expand All @@ -64,11 +73,26 @@ CREATE TABLE IF NOT EXISTS `reference` (
(kind <> 'Tag' OR (kind = 'Tag' AND remote IS NULL))
)
);

CREATE TABLE IF NOT EXISTS `reflog` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`ref_name` TEXT NOT NULL,
`old_oid` TEXT NOT NULL,
`new_oid` TEXT NOT NULL,
`committer_name` TEXT NOT NULL,
`committer_email` TEXT NOT NULL,
`timestamp` INTEGER NOT NULL,
`action` TEXT NOT NULL,
`message` TEXT NOT NULL
);

-- (name, kind, remote) as unique key when remote is not null
CREATE UNIQUE INDEX idx_name_kind_remote ON `reference`(`name`, `kind`, `remote`)
WHERE `remote` IS NOT NULL;

-- (name, kind) as unique key when remote is null
CREATE UNIQUE INDEX idx_name_kind ON `reference`(`name`, `kind`)
WHERE `remote` IS NULL;

CREATE INDEX idx_ref_name_timestamp ON `reflog`(`ref_name`, `timestamp`);
```
1 change: 1 addition & 0 deletions aria/lib/routes-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const ROUTES: EachRoute[] = [
{ title: "pull", href: "/pull" },
{ title: "push", href: "/push" },
{ title: "rebase", href: "/rebase" },
{ title: "reflog", href: "/reflog" },
{ title: "remote", href: "/remote" },
{ title: "reset", href: "/reset" },
{ title: "restore", href: "/restore" },
Expand Down
15 changes: 14 additions & 1 deletion libra/sql/sqlite_20240331_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,23 @@ CREATE TABLE IF NOT EXISTS `reference` (
(kind <> 'Tag' OR (kind = 'Tag' AND remote IS NULL))
)
);
CREATE TABLE IF NOT EXISTS `reflog` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`ref_name` TEXT NOT NULL,
`old_oid` TEXT NOT NULL,
`new_oid` TEXT NOT NULL,
`committer_name` TEXT NOT NULL,
`committer_email` TEXT NOT NULL,
`timestamp` INTEGER NOT NULL,
`action` TEXT NOT NULL,
`message` TEXT NOT NULL
);
-- (name, kind, remote) as unique key when remote is not null
CREATE UNIQUE INDEX idx_name_kind_remote ON `reference`(`name`, `kind`, `remote`)
WHERE `remote` IS NOT NULL;

-- (name, kind) as unique key when remote is null
CREATE UNIQUE INDEX idx_name_kind ON `reference`(`name`, `kind`)
WHERE `remote` IS NULL;
WHERE `remote` IS NULL;

CREATE INDEX idx_ref_name_timestamp ON `reflog`(`ref_name`, `timestamp`)
3 changes: 3 additions & 0 deletions libra/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ enum Commands {
Remote(command::remote::RemoteCmds),
#[command(about = "Manage repository configurations")]
Config(command::config::ConfigArgs),
#[command(about = "Manage the log of reference changes (e.g., HEAD, branches)")]
Reflog(command::reflog::ReflogArgs),

// other hidden commands
#[command(
Expand Down Expand Up @@ -137,6 +139,7 @@ pub async fn parse_async(args: Option<&[&str]>) -> Result<(), GitError> {
Commands::Pull(args) => command::pull::execute(args).await,
Commands::Config(args) => command::config::execute(args).await,
Commands::Checkout(args) => command::checkout::execute(args).await,
Commands::Reflog(args) => command::reflog::execute(args).await,
}
Ok(())
}
Expand Down
28 changes: 24 additions & 4 deletions libra/src/command/cherry_pick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use mercury::internal::object::tree::{Tree, TreeItem, TreeItemMode};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use sea_orm::{ConnectionTrait};
use crate::internal::reflog::{with_reflog, ReflogAction, ReflogContext};

/// Arguments for the cherry-pick command
#[derive(Parser, Debug)]
Expand Down Expand Up @@ -203,7 +205,25 @@ async fn create_cherry_pick_commit(
);

save_object(&commit, &commit.id).map_err(|e| format!("failed to save commit: {e}"))?;
update_head(&commit.id.to_string()).await;

// let reflog extract the subject of message.
let action = ReflogAction::CherryPick { source_message: original_commit.message.clone() };
let context = ReflogContext {
old_oid: parent_id.to_string(),
new_oid: commit.id.to_string(),
action,
};

with_reflog(
context,
move |txn| {
Box::pin(async move {
update_head(txn, &commit.id.to_string()).await;
Ok(())
})
},
true,
).await.map_err(|e| e.to_string())?;
Ok(commit.id)
}

Expand Down Expand Up @@ -388,8 +408,8 @@ async fn resolve_commit(reference: &str) -> Result<SHA1, String> {
/// This function updates the current branch to point to the specified commit.
/// It only works when HEAD is pointing to a branch (not in detached HEAD state).
/// The branch reference is updated to the new commit ID.
async fn update_head(commit_id: &str) {
if let Head::Branch(name) = Head::current().await {
Branch::update_branch(&name, commit_id, None).await;
async fn update_head<C: ConnectionTrait>(db: &C, commit_id: &str) {
if let Head::Branch(name) = Head::current_with_conn(db).await {
Branch::update_branch_with_conn(db, &name, commit_id, None).await;
}
}
125 changes: 81 additions & 44 deletions libra/src/command/clone.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
use super::fetch::{self};
use crate::command::restore::RestoreArgs;
use crate::command::{self, branch};
use crate::internal::branch::Branch;
use crate::internal::config::{Config, RemoteConfig};
use crate::internal::head::Head;
use crate::internal::reflog::{with_reflog, zero_sha1, ReflogAction, ReflogContext};
use crate::utils::path_ext::PathExt;
use crate::utils::util;
use clap::Parser;
use colored::Colorize;
use scopeguard::defer;
use sea_orm::DatabaseTransaction;
use std::cell::Cell;
use std::path::PathBuf;
use std::{env, fs};

use super::fetch::{self};

const ORIGIN: &str = "origin"; // default remote name, prevent spelling mistakes

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -97,59 +98,95 @@ pub async fn execute(args: CloneArgs) {
name: "origin".to_string(),
url: remote_repo.clone(),
};
fetch::fetch_repository(&remote_config, args.branch.clone()).await;
fetch::fetch_repository(remote_config, args.branch.clone()).await;

/* setup */
setup(remote_repo.clone(), args.branch.clone()).await;
if let Err(e) = setup_repository(remote_repo.clone(), args.branch.clone()).await {
eprintln!("fatal: {}", e);
return;
}

is_success.set(true);
}

async fn setup(remote_repo: String, specified_branch: Option<String>) {
// look for remote head and set local HEAD&branch
let remote_head = Head::remote_current(ORIGIN).await;

// set config: remote.origin.url,it's essential for git
Config::insert("remote", Some(ORIGIN), "url", &remote_repo).await;
// set config: remote.origin.fetch
// todo: temporary ignore fetch option

if let Some(specified_branch) = specified_branch {
setup_branch(specified_branch).await;
} else if let Some(Head::Branch(name)) = remote_head {
setup_branch(name).await;
} else if let Some(Head::Detached(_)) = remote_head {
eprintln!("fatal: remote HEAD points to a detached commit");
/// Sets up the local repository after a clone by configuring the remote,
/// setting up the initial branch and HEAD, and creating the first reflog entry.
async fn setup_repository(remote_repo: String, specified_branch: Option<String>) -> Result<(), String> {
let db = crate::internal::db::get_db_conn_instance().await;
let remote_head = Head::remote_current_with_conn(db, ORIGIN).await;

// Determine which branch to check out.
let branch_to_checkout = match specified_branch {
Some(b_name) => Some(b_name),
None => {
if let Some(Head::Branch(name)) = remote_head {
Some(name)
} else {
None // This case handles empty repos or detached HEADs
}
}
};

if let Some(branch_name) = branch_to_checkout {
let origin_branch = Branch::find_branch_with_conn(db, &branch_name, Some(ORIGIN)).await
.ok_or_else(|| format!("fatal: remote branch '{}' not found.", branch_name))?;

// Prepare the reflog context *before* the transaction
let action = ReflogAction::Clone { from: remote_repo.clone() };

let context = ReflogContext {
// In a clone, there is no "old" oid. A zero-hash is the standard representation.
old_oid: zero_sha1().to_string(),
new_oid: origin_branch.commit.to_string(),
action,
};

// `insert_ref` is true, as we are creating the initial branch reflog.
with_reflog(
context,
move |txn: &DatabaseTransaction| {
Box::pin(async move {
// 1. Create the local branch pointing to the fetched commit
Branch::update_branch_with_conn(txn, &branch_name, &origin_branch.commit.to_string(), None).await;

// 2. Set HEAD to point to the new local branch
Head::update_with_conn(txn, Head::Branch(branch_name.to_owned()), None).await;

// 3. Configure remote tracking for the branch
let merge_ref = format!("refs/heads/{}", branch_name);
Config::insert_with_conn(txn, "branch", Some(&branch_name), "merge", &merge_ref).await;
Config::insert_with_conn(txn, "branch", Some(&branch_name), "remote", ORIGIN).await;

// 4. Configure the remote URL
Config::insert_with_conn(txn, "remote", Some(ORIGIN), "url", &remote_repo).await;
Ok(())
})
},
true,
).await.map_err(|e| e.to_string())?;

// After the DB is set up, restore the working directory
command::restore::execute(RestoreArgs {
worktree: true,
staged: true,
source: None,
pathspec: vec![util::working_dir_string()],
}).await;

} else {
println!("warning: You appear to have cloned an empty repository.");

// set config: branch.$name.merge, e.g.
let merge = "refs/heads/master".to_owned();
Config::insert("branch", Some("master"), "merge", &merge).await;
// set config: branch.$name.remote
Config::insert("branch", Some("master"), "remote", ORIGIN).await;
// We only need to set the remote URL. No reflog is created as there are no commits.
Config::insert("remote", Some(ORIGIN), "url", &remote_repo).await;

// Optionally set up a default branch config for a future 'master' or 'main'
let default_branch = "master";
let merge_ref = format!("refs/heads/{}", default_branch);
Config::insert("branch", Some(default_branch), "merge", &merge_ref).await;
Config::insert("branch", Some(default_branch), "remote", ORIGIN).await;
}
}

async fn setup_branch(branch_name: String) {
let origin_head_branch = Branch::find_branch(&branch_name, Some(ORIGIN))
.await
.expect("origin HEAD branch not found");

Branch::update_branch(&branch_name, &origin_head_branch.commit.to_string(), None).await;
Head::update(Head::Branch(branch_name.to_owned()), None).await;

let merge = "refs/heads/".to_owned() + &branch_name;
Config::insert("branch", Some(&branch_name), "merge", &merge).await;
Config::insert("branch", Some(&branch_name), "remote", ORIGIN).await;

command::restore::execute(RestoreArgs {
worktree: true,
staged: true,
source: None,
pathspec: vec![util::working_dir_string()],
})
.await;
Ok(())
}

/// Unit tests for the clone module
Expand Down
Loading