Skip to content
Open
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
92 changes: 92 additions & 0 deletions HenriqueMC17/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 🌌 µgit (ugit) — Um Git Minimalista de Alta Fidelidade

Uma implementação compacta e de alta fidelidade do Git escrita do zero em **TypeScript** e **Node.js**, utilizando compactação nativa e o algoritmo de comparação de **Myers** (*Myers Diff Algorithm*).

---

## 🛠️ Stack Tecnológica

* **Linguagem & Ambiente:** TypeScript (Node.js) para tipagem estática robusta e modularização limpa.
* **Armazenamento e Hashing:** Algoritmo SHA-1 nativo via módulo `crypto` do Node.
* **Compactação:** Deflate/Inflate nativo do Zlib para persistir os snapshots em arquivos compactados, replicando o comportamento interno do Git clássico.
* **Algoritmo do Diff:** Implementação manual do algoritmo guloso de **Myers** para cálculo de menor sequência de edição (*Shortest Edit Script*).

---

## 🏛️ Arquitetura de Snapshot e Armazenamento

Em conformidade com a arquitetura interna do Git, o `ugit` trata arquivos como dados endereçáveis por conteúdo (*content-addressable storage*):

```text
.ugit/
├── HEAD <-- Ponteiro para o commit ativo (SHA-1 em texto puro)
├── index <-- JSON que mapeia os caminhos dos arquivos para seus respectivos blobs staged
└── objects/
├── 4b/
│ └── 825dc... <-- Objetos (blobs, trees ou commits) indexados pelos 2 primeiros caracteres do SHA-1
└── ...
```

### O Modelo de Objetos (Object Model)

Cada arquivo gravado no repositório é compactado e armazenado na pasta `.ugit/objects/` em arquivos cujos nomes correspondem ao hash SHA-1 de 40 caracteres (dividido em subpastas de 2 caracteres). O cabeçalho dos arquivos segue a convenção do Git clássico: `<tipo> <tamanho>\0<conteúdo>`.

* **Blob:** Salva apenas o snapshot do conteúdo puro de um arquivo.
* **Tree:** Salva a estrutura de diretórios recursiva. O `ugit` constrói e resolve árvores de diretórios recursivamente. Cada entrada em uma árvore contém as permissões (`100644` ou `040000`), tipo (`blob` ou `tree`), hash SHA-1 e o nome do arquivo/pasta correspondente.
* **Commit:** Salva os metadados contendo o hash da árvore raiz (`tree`), commits pais (`parent`), nome do autor, timestamp Epoch e a mensagem de commit.

---

## 🧬 Abordagem do Diff: Algoritmo de Myers

Diferente de implementações simples de diff linha a linha que sofrem em casos de ambiguidade, o `ugit` implementa o **Myers Diff Algorithm**.

* O algoritmo funciona mapeando o problema de encontrar a menor diferença de texto como a busca de um caminho mínimo em uma grade bidimensional (DAG).
* Realiza uma busca em largura (*Breadth-First Search*) otimizada sobre as diagonais da grade para encontrar o caminho com menor custo de edições (inserções e remoções).
* Uma vez encontrado o ponto final, reconstrói o caminho backtrackando através do histórico das diagonais alcançadas, garantindo um diff limpo, coeso e altamente legível.

---

## ⚡ Exemplo Prático de Uso

### 1. Pré-requisitos & Compilação

Instale as dependências de desenvolvimento do TypeScript e compile o projeto para JavaScript puro:

```bash
npm install
npm run build
```

### 2. Fluxo Básico do Terminal

```bash
# Inicializa o repositório µgit
node dist/index.js init

# Cria um arquivo de teste
echo "Olá Mundo!" > teste.txt

# Adiciona à staging area
node dist/index.js add teste.txt

# Verifica o status
node dist/index.js status

# Commita a alteração
node dist/index.js commit -m "Meu primeiro commit com ugit"

# Mostra o histórico
node dist/index.js log

# Modifica o arquivo e roda o diff
echo "Olá Mundo com Myers!" > teste.txt
node dist/index.js diff
```

---

## 🧠 Aprendizados

* **O que funcionou:** A modularidade do TypeScript se mostrou excelente para organizar as responsabilidades (divisão de responsabilidades entre CLI parser, armazenamento de objetos e staging area). A compactação zlib nativa se encaixou perfeitamente no formato tradicional e garantiu que o projeto se comportasse de forma idêntica à especificação interna do Git.
* **O que mudaria em uma versão 2.0:** A staging area (`index`) foi simplificada como um arquivo JSON para facilitar a depuração. Em uma versão comercial de grande escala, usaríamos o formato de index binário do Git com cabeçalho de checksum SHA-1 para otimizar a velocidade de E/S de arquivos sob diretórios gigantes.
20 changes: 20 additions & 0 deletions HenriqueMC17/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "ugit",
"version": "1.0.0",
"description": "A micro-Git implementation in TypeScript showcasing Myers Diff and content-addressable storage",
"main": "dist/index.js",
"bin": {
"ugit": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"ugit": "ts-node src/index.ts"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^20.11.24",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
87 changes: 87 additions & 0 deletions HenriqueMC17/src/index-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as fs from 'fs';
import * as path from 'path';
import { Index } from './types';
import { ObjectStore } from './object-store';

export class IndexManager {
private static getIndexPath(repoRoot: string): string {
return path.join(repoRoot, '.ugit', 'index');
}

/**
* Carrega o arquivo index (staging area).
*/
public static readIndex(repoRoot: string): Index {
const indexPath = this.getIndexPath(repoRoot);
if (!fs.existsSync(indexPath)) {
return { entries: {} };
}
try {
const content = fs.readFileSync(indexPath, 'utf-8');
return JSON.parse(content) as Index;
} catch {
return { entries: {} };
}
}

/**
* Grava as alterações de volta no arquivo index.
*/
public static writeIndex(repoRoot: string, index: Index): void {
const indexPath = this.getIndexPath(repoRoot);
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
}

/**
* Adiciona um arquivo específico (ou todos via recursividade) à staging area.
*/
public static add(repoRoot: string, targetPath: string): void {
const absolutePath = path.resolve(repoRoot, targetPath);

if (!fs.existsSync(absolutePath)) {
throw new Error(`Caminho inexistente: ${targetPath}`);
}

const index = this.readIndex(repoRoot);
const stats = fs.statSync(absolutePath);

if (stats.isFile()) {
this.stageFile(repoRoot, index, absolutePath);
} else if (stats.isDirectory()) {
this.stageDirectory(repoRoot, index, absolutePath);
}

this.writeIndex(repoRoot, index);
}

private static stageFile(repoRoot: string, index: Index, absoluteFilePath: string): void {
// Ignora a própria pasta do .ugit e o node_modules
if (absoluteFilePath.includes(path.join(repoRoot, '.ugit')) || absoluteFilePath.includes('node_modules')) {
return;
}

const relativePath = path.relative(repoRoot, absoluteFilePath).replace(/\\/g, '/');
const content = fs.readFileSync(absoluteFilePath);

// Grava o objeto blob
const sha1 = ObjectStore.writeObject(repoRoot, 'blob', content);
index.entries[relativePath] = sha1;
}

private static stageDirectory(repoRoot: string, index: Index, absoluteDirPath: string): void {
const files = fs.readdirSync(absoluteDirPath);
for (const file of files) {
const fullPath = path.join(absoluteDirPath, file);
const stats = fs.statSync(fullPath);

if (stats.isFile()) {
this.stageFile(repoRoot, index, fullPath);
} else if (stats.isDirectory()) {
if (file === '.ugit' || file === 'node_modules' || file === '.git') {
continue;
}
this.stageDirectory(repoRoot, index, fullPath);
}
}
}
}
Loading