Manipulando Contribuições Locais do Git com Go
Tags: go, braziliandevs, git, tutorial
Published: 2024-02-04 15:30:14

A inspiração para este projeto surgiu de uma necessidade pessoal: eu queria uma maneira eficiente de verificar quantos commits eu havia feito em um dia, considerando que meus repositórios não se limitam apenas ao GitHub, mas também incluem o GitLab e outros. 

Assim, embarquei na jornada de manipular o .git dos repositórios, utilizando Go para simplificar e aprimorar esse processo.

# Entendendo arquivos .git

O Git desempenha um papel crucial como sistema de controle de versão em projetos colaborativos de desenvolvimento. Seu principal objetivo é rastrear e documentar as diversas alterações realizadas ao longo do tempo. Essa funcionalidade é especialmente valiosa em contextos nos quais várias pessoas e equipes contribuem para um projeto comum.

Uma das características marcantes do Git é a sua capacidade de proporcionar aos desenvolvedores uma visão abrangente da linha do tempo do projeto. Isso inclui todas as alterações feitas, decisões tomadas e progresso alcançado. Ao consolidar essas informações em um único local, o Git simplifica a compreensão do desenvolvimento do projeto, oferecendo uma referência clara e organizada para todas as partes envolvidas.

Essa abordagem permite que as equipes visualizem e compreendam não apenas as modificações específicas, mas também a trajetória geral do projeto. Consequentemente, os desenvolvedores podem colaborar de maneira mais eficiente, alinhando suas atividades de acordo com o panorama completo do projeto. Em resumo, o Git se destaca como uma ferramenta essencial, proporcionando transparência e eficácia na gestão de mudanças ao longo do ciclo de vida do desenvolvimento de software colaborativo.

## Falando um pouco sobre repositórios

Em sua essência, um repositório abrange a totalidade dos arquivos e pastas relacionados a um projeto, acompanhados pelo histórico de revisões de cada arquivo. Esse histórico é registrado como instâncias no tempo, chamadas de commits, cada uma representando uma alteração específica. 

Esses commits, por sua vez, podem ser estruturados em diferentes linhas de desenvolvimento conhecidas como branches, proporcionando uma abordagem organizada e ramificada para o progresso do projeto ao longo do tempo.

O Git armazena informações essenciais dentro do diretório .git no repositório local, utilizando uma estrutura interna organizada para manter controle sobre o histórico, configurações e outros dados relacionados ao projeto.

Algumas das principais subpastas e arquivos dentro do diretório .git incluem:

- objects: Esta pasta contém todos os objetos fundamentais do Git, como blobs (dados binários), trees (estruturas de diretórios), e commits. Cada objeto é identificado por um hash único, garantindo integridade e rastreabilidade.
- refs: Aqui, são armazenadas as referências, incluindo as branches, tags e HEAD (indicando a branch atual). Cada referência aponta para um commit específico, facilitando a navegação no histórico do projeto.
- HEAD: Este arquivo aponta para a branch atual, indicando qual commit está atualmente em foco. Isso permite ao Git saber onde novos commits devem ser registrados.
- config: Armazena as configurações específicas do repositório, como informações do usuário, configurações globais e comportamentos específicos do Git para esse projeto.
- index: Também conhecido como "staging area", esse arquivo mantém o estado atual do diretório de trabalho e lista os arquivos que serão incluídos no próximo commit. Permite aos desenvolvedores preparar suas alterações antes de registrá-las no histórico.
- hooks: Esta pasta contém scripts personalizados que podem ser executados automaticamente em determinados eventos do Git, como antes ou depois de um commit.

O cuidadoso gerenciamento desses componentes no diretório `.git` é fundamental para manter a integridade do histórico e garantir um controle de versão eficiente durante o desenvolvimento colaborativo de projetos.

# Começando

Neste projeto inicio explorando o pacote flag da linguagem Go. Em muitas plataformas, os utilitários de linha de comando têm a capacidade de aceitar sinalizadores (flags), oferecendo uma maneira flexível de personalizar a execução de comandos.

Os sinalizadores são strings que contêm valores associados a chaves específicas e são adicionados à linha de comando após o nome do comando. No contexto da linguagem de programação Go, podemos criar utilitários de linha de comando que suportam sinalizadores utilizando o pacote flag da biblioteca padrão.

Este pacote facilita a manipulação e análise de sinalizadores, tornando a construção de aplicativos de linha de comando mais eficiente. Vamos explorar como podemos aproveitar o pacote flag para criar utilitários poderosos que aceitam sinalizadores de forma intuitiva e eficaz.


```go
package main

import (
	"flag"
)

func main() {
	var folder string
	// adiciona uma flag 
	flag.StringVar(&folder, "add", "", "recebe um novo diretório para análise")
	flag.Parse()
}
```

rimeiramente irei receber o diretório para analisar os logs do repositório git, então preciso saber se no diretório recebido realmente tem um arquivo `.git`. Essa parte é bem simples usamos o pacote `os` com a função `ReadDir()` para analisar o diretório e para isso criamos a função `verifierDir()`.

```go
func verifierDir(folder string) bool {
	f, err := os.Open(folder) // abre o diretório recebido

	if err != nil {
		panic(err)
	}

	// verifica se nesse diretório existe um .git
	for {
		files, err := f.ReadDir(1)
		if err != nil{
			if err == io.EOF {
				break
			}
			continue
		}
	
		if file[0].Name() == ".git" {
			return true
		}
	}

	return false
}
``` 

---

Agora que já temos a verificação que existe um `.git` dentro do diretório passado temos que pegar os logs dos commits realizados nesse repositório para isso vou utilizar o pacote `go-git`.

O pacote `go-git` é uma biblioteca em Go projetada para facilitar a manipulação de repositórios Git. Ele fornece uma interface programática que simplifica a interação com repositórios Git, sendo particularmente útil para automação em aplicativos Go. Ao abstrair muitos dos detalhes do Git, o `go-git` permite que os desenvolvedores se concentrem nas tarefas específicas que desejam realizar, tornando mais eficiente a implementação de funcionalidades relacionadas ao controle de versão em suas aplicações.

Irei criar a função `getCommits()`  que usara o pacote `go-git` para pegar e filtrar os logs dos commits por dia e quantidade naquele repositório passado pelo pacote `flag`.

```go
// struct do tipo Commit para receber dados do iteravel
type Commmit struct {
	Key   time.Time
	Amount int
}

func getCommits(folder string) []Commit {
	// abre o repositório .git
	repo, err := git.PlainOpen(folder)

	// err
	if err != nil { log.Fatal(err) }

	// aponta para o branch atual
	ref, err := repo.Head()

	//err
	if err != nil { log.Fatal(err) }

	// cria um iteravel para percorrer os commits do repositório
	cIter, err := repo.Log(&git.LogOptions{From: ref.Hash()})

	if err != nil { log.Fatal(err) }

	// inicia um type Commit previamente criado para receber dados
	SliceCommit := []Commit{}

	// variaveis auxiliares
	var temp, tempFormat time.Time

	err = cIter.ForEach(func (commit *object.Commit) error {
		// verifica se é a primeira iteração com o laço
		if len(SliceCommit) == 0{
			// recebe o primeiro valor do iteravel
			temp = commit.Author.When
			// Adiciona o primeiro como key dentro do tipo Commit
			SliceCommit = append(SliceCommit, Commmit{commit.Author.When, 1})
		} else {
			// verifica se o dia é diferente
			if tempFormat.Format("01/02/2006") != commit.Author.When.Format("01/02/2006") {
				// se o dia for diferente adiciona um novo valor de key
			    SliceCommit = append(SliceCommit, Commmit{commit.Author.When, 1})  
			    temp = commit.Author.When  
			} else {  
				// se for o mesmo dia adiciona mais 1 na quantidade de commits do dia
			    for i := range SliceCommit {  
			       if SliceCommit[i].Key == temp {  
			          SliceCommit[i].Amount += 1  
			          break  
				    }  
				}  
			}
		}
		// recebe o prox commit para iteração
		tempFormat = commit.Author.When
		return nil
	})

	return SliceCommit
}
```

Nesta função optei por criar uma `struct` formando o tipo `Commit` para receber os dados do  iterável, também seria possível usar um `map`  para armazenar os valores linkados a chaves mas o `map` em Go tem um grave problema ele não tem uma ordenação especifica então já que o iterável me retorna já ordenado os dados optei pela criação de uma `struct`.

Com isso a parte mais complexa esta finalizada. Apenas para teste aqui esta a primeira parte do código.

```go
package main  
  
import (  
    "flag"  
    "fmt"    "gopkg.in/src-d/go-git.v4"    "gopkg.in/src-d/go-git.v4/plumbing/object"    "io"    "log"    "os"    "time")  
  
type Commmit struct {  
    Key   time.Time  
    Amount int  
}  
  
func verifierDir(folder string) bool {  
    f, err := os.Open(folder)  
  
    if err != nil {  
       panic(err)  
    }  
  
    for {  
       files, err := f.ReadDir(1)  
       if err != nil {  
          if err == io.EOF {  
             break  
          }  
          continue  
       }  
  
       if files[0].Name() == ".git" {  
          return true  
       }  
    }  
  
    return false  
}  
  
func getCommits(folder string) []Commmit {  
    repo, err := git.PlainOpen(folder)  
  
    if err != nil {  
       log.Fatal(err)  
    }  
  
    ref, err := repo.Head()  
  
    if err != nil {  
       log.Fatal(err)  
    }  
  
    cIter, err := repo.Log(&git.LogOptions{From: ref.Hash()})  
  
    if err != nil {  
       log.Fatal(err)  
    }  
  
    SliceCommit := []Commmit{}  
    var temp, tempFormat time.Time  
    err = cIter.ForEach(func(commit *object.Commit) error {  
       if len(SliceCommit) == 0 {  
          temp = commit.Author.When  
          SliceCommit = append(SliceCommit, Commmit{commit.Author.When, 1})  
       } else {  
          if tempFormat.Format("01/02/2006") != commit.Author.When.Format("01/02/2006") {  
             SliceCommit = append(SliceCommit, Commmit{commit.Author.When, 1})  
             temp = commit.Author.When  
          } else {  
             for i := range SliceCommit {  
                if SliceCommit[i].Key == temp {  
                   SliceCommit[i].Amount += 1  
                   break  
                }  
             }  
          }  
       }  
       tempFormat = commit.Author.When  
       return nil  
    })  
  
    return SliceCommit  
}  
  
func main() {  
    var folder string  
    flag.StringVar(&folder, "add", "", "novo diretório para análise")  
    flag.Parse()  
    if verifierDir(folder) {  
       SliceCommit := getCommits(folder)  
       for i := range SliceCommit {  
          fmt.Printf("%q ----- %d\n", SliceCommit[i].Key, SliceCommit[i].Amount)  
       }  
  
    }  
}
```
# Criação e Renderização do Gráfico

Bom agora vamos para a parte mais pratica em si, que seria criar e renderizar a tabela gráfica.

Para renderizar tabelas no terminal vou utilizar o pacote `go-pretty` basicamente proporciona utilitários para embelezar a saída do console de tabelas, listas, barras de progresso, texto, etc., com grande ênfase na personalização.

Vou começar criando a função `tableCommits()` para criar um ``Slice`` para implementarmos no pacote `go-pretty`.

```go
func tableCommits(SliceCommit []Commmit, weeks string) { 
	// inicia a variavel now com o tempo atual
    now := time.Now()  
    // inicia a variavel ant 
    var ant time.Time  

	// transforma o weeks string em numWeeks inteiro
    numWeeks, err := strconv.Atoi(weeks)  

    if err != nil {  
       panic(err)  
    }  

	// determina qual dia da semana é para determinar quanto sera o ant
    switch now.Format("Monday") {  
    case "Sunday":  
       ant = now.AddDate(0, 0, -8*7)  
    case "Monday":  
       ant = now.AddDate(0, 0, -1+(-numWeeks*7))  
    case "Tuesday":  
       ant = now.AddDate(0, 0, -2+(-numWeeks*7))  
    case "Wednesday":  
       ant = now.AddDate(0, 0, -3+(-numWeeks*7))  
    case "Thursday":  
       ant = now.AddDate(0, 0, -4+(-numWeeks*7))  
    case "Friday":  
       ant = now.AddDate(0, 0, -5+(-numWeeks*7))  
    case "Saturday":  
       ant = now.AddDate(0, 0, -6+(-numWeeks*7))  
    }  

	// inicia o Slice para formular a tabela
    dateCommit := []string{}  
	// cria uma flag para controle de condição
    flag := false  
	// add 1 dia na data atual para pegar o dia atual na função Before()
    now = now.AddDate(0, 0, 1)  
    for ant.Before(now) {  
       for _, commit := range SliceCommit {  
	       // verifica se o dia existe dentro dos commits
          if commit.Key.Format("02/01/2006") == ant.Format("02/01/2006") {  
             dateCommit = append(dateCommit, "( "+ant.Format("02-01")+" - "+"["+strconv.Itoa(commit.Amount)+"]"+" )")  
             flag = true  
          }  
       }  
       // se o dia nao existeir coloca apenas o dia sem a informação de commits
       if flag == false {  
          dateCommit = append(dateCommit, "( "+ant.Format("02")+" )")  
       }  
       // volta a flag para falso
       flag = false  
       // add 1 dia em ant para o loop funcionar
       ant = ant.AddDate(0, 0, 1)  
    }  
    // chama a função que renderiza a tabela.
    createTable(dateCommit)  
}
```

Essa função não tão é complexa, primeiramente ela determina qual é o dia da semana especifico e cria uma variável `ant` que determina o dia inicial da tabela para mostrar os commits que foi definida pela flag `weeks`. 

Após isso ela define uma nova variável `dateCommit` que a disposição dos dados para formação da tabela e então chama a função `createTable()` passando essa  variável para renderizar a tabela.

Com essa variável definida podemos passar pra parte de renderizar a tabela no terminal usando  o pacote `go-pretty` na função `createTable()`.

```go
func createTable(dateCommit []string) {  
	// inicia uma nova instância do escritor de tabela.
    t := table.NewWriter()  
    
    // define a saída da tabela para o console padrão.
    t.SetOutputMirror(os.Stdout) 

	// adiciona um cabeçalho à tabela com os nomes dos dias da semana.
    t.AppendHeader(table.Row{"  Sunday  ", "  Monday  ", "  Tuesday  ", "  Wednesday  ", "  Thursday  ", "  Friday  ", "  Saturday  "}) 
     
	// inicializa uma variável temporária para armazenar as linhas da tabela.
    var tempRow []interface{}  

	// itera sobre os commits fornecidos para criar as linhas da tabela.
    for i, commit := range dateCommit { 
	   //  adiciona cada commit à linha temporária. 
       tempRow = append(tempRow, commit)  

       // verifica se atingiu o final de uma semana ou a última iteração.
       if (i+1)%7 == 0 || i+1 == len(dateCommit) {  
          t.AppendRow(tempRow)  
          tempRow = []interface{}{}  
       }  
    }  

	// configura a formatação das colunas da tabela, alinhamento e outros detalhes.
    t.SetColumnConfigs([]table.ColumnConfig{  
       {Number: 1, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 2, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 3, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 4, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
       {Number: 7, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},  
    })  
    // define o estilo da tabela.
    t.SetStyle(table.StyleColoredRedWhiteOnBlack)

	// renderiza a tabela e a exibe no console.
    t.Render()  
}
```

De forma simples essa função cria visualmente uma tabela organizada dos commits, dividida por dias da semana utilizando a variável  `dateCommit`.

# Conclusão

Esse projeto foi mais para otimizar um problema e aumentar a qualidade de informações que eu sabia sobre Go e Git.

Inspiração: https://flaviocopes.com/go-git-contributions/

Repositório do Projeto: https://github.com/darlangui/localgit.git