DEV Community

Darlan Guimarães
Darlan Guimarães

Posted on • Edited on

Manipulando Contribuições Locais do Git com Go

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.

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()
}
Enter fullscreen mode Exit fullscreen mode

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().

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
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
}
Enter fullscreen mode Exit fullscreen mode

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.

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)  
       }  

    }  
}
Enter fullscreen mode Exit fullscreen mode

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.

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)  
}
Enter fullscreen mode Exit fullscreen mode

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().

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()  
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)