Docker en Raspberry Pi

Actualizado el 27 de Junio de 2024

Introducción

Es posible usar Docker con Raspberry Pi, puede parecer que en un dispositivo de prestaciones limitadas no tenga mucho sentido pero cada vez es más común verlo para dispositivos IoT y los dispositivos Raspberry han mejorado mucho con el tiempo sobre todo a partir de Raspberry Pi 4.

Aquí coloco algunas notas de como configurar Docker y algunas posibles mejoras de rendimiento.

Preparando el sistema

Lo primero es actualizar el sistema y reiniciar.

sudo apt-get update
sudo apt-get upgrade

Instalando Docker

Instalamos Docker usando el script proporcionado por ellos.

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Después agregamos al usuario pi al grupo docker haciendo:

 sudo usermod -aG docker pi

Una vez agregado si estamos con el usuario tendremos que volver a iniciar sesión o reiniciar para que los cambios de pertenencia al nuevo grupo se hagan efectivos.

Reiniciamos con sudo reboot now y verificamos la versión Docker con docker version.
Probamos también a verificar el sistema usando este comando que descargará una imagen Docker y la ejecutará, se verá un mensaje como Hello from Docker!

docker run --rm hello-world

Instalando Docker-Compose

Ahora instalamos Docker Compose que nos hará falta para gestionar varios contenedores a la vez y la comunicación entre ellos, primero instalamos pip, el gestor de paquetes de Python, cualquier distribución de Raspberry ya viene con Python instalado así que no hará falta instalarlo.

sudo curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && sudo python3 get-pip.py

Si tenemos algún error podemos probar a ejecutar este comando y después volvemos a ejecutar el comando anterior para actualizar pip.

sudo apt-get install python3-pip

Es importante que en el fichero /etc/pip.conf tengamos agregado el repositorio de piwheels

[global]
extra-index-url=https://www.piwheels.org/simple

Es posible también que tengamos que instalar distutils

sudo apt-get install python3-distutils

Ahora instalamos Docker Compose

python3 -m pip install docker-compose

Si nos da este error: command ‘arm-linux-gnueabihf-gcc’ failed with exit status 1 podemos probar lo siguiente y repetir el comando anterior:

sudo apt-get install libzbar-dev libzbar0

Si nos da un error relacionado con python.h instalamos este paquete:

sudo apt-get install python3-dev

Si tenemos un error relacionado con ffi.h instalamos este paquete:

apt install libffi-dev

Finalmente para verificar que docker-compose se ha instalado bien se puede usar:

docker-compose --version

Rendimiento

Usando ZRAM

A veces puede suceder que zswap no nos de el resultado esperado, podemos probar en ese caso con zram, para eso seguimos estos pasos:

sudo wget -O /usr/bin/zram.sh https://raw.githubusercontent.com/Bash-Projects/rpi_zram/master/zram.sh

Damos permisos para ejecutar el script que nos acabamos de descargar:

sudo chmod +x /usr/bin/zram.sh

Ahora lo vamos a programar para que se ejecute 50 segundos después del arranque, así que lanzamos crontab.

sudo crontab -e

y programamos la ejecución del script:

@reboot ( sleep 50 ; sudo /usr/bin/zram.sh &)

El comando lo podemos lanzar directamente para ver el resultado o podemos reiniciar y esperar 50 segundos para ver el resultado, podemos verificar el aumento de memoria usando:

free -h

para ver como ha aumentado la RAM y para poder ver el aumento en la swap usamos:

swapon -s

Recomendaciones

Una serie de recomendaciones y problemas frecuentes.

Usar gosu en lugar de sudo

Las imágenes Docker normalmente se ejecutan como root pero a veces hace falta ejecutar algunos comandos como un usuario concreto dentro del entry point en este caso la mejor opción es usar gosu en lugar de sudo.

Problemas

Job for docker.service failed because the control process exited with error code.
See «systemctl status docker.service» and «journalctl -xe» for details.

Si el servicio da errores al arrancar podemos usar journalctl -xe y si vemos un error como The process’ exit code is ‘exited’ and its exit status is 2, podemos intentar lo siguiente:

rm -rf /var/lib/docker
sudo curl -sSL https://get.docker.com | sh
service docker restart

Es decir, borramos la carpeta, volvemos a instalar y reiniciamos el servicio, es posible que este error indique la tarjeta SD está fallando.

Errores al subir imágenes

Si al hacer push da un error como file integrity checksum failed for «usr/lib/arm-linux-gnueabihf/libicui18n.so.66.1» ejecutamos este comando:

docker system prune -a

Si aún así continuamos con el error no nos quedará otra que borrar la imagen y volverla a crear con docker build.

At least one invalid signature was encountered

Si durante la creación de imágenes obtenemos un error «At least one invalid signature was encountered» al instalar paquetes via apt-get es necesario instalar la versión actualizada de libseccomp2 la opción recomendada es descargar l paquete (en este caso via wget) e instalarlo desde aquí aquí.
Es posible que la versión del paquete haya cambiado y no se encuentre en el servidor, en ese caso se puede acceder a más mirrors aquí.

wget  http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.4.4-1+b1_armhf.deb
sudo apt-get install ./libseccomp2_2.4.4-1+b1_armhf.deb

E: Release file for xxx is not valid yet (invalid for another 2h 45min 28s). Updates for this repository will not be applied.

Esto es por un problema con la hora del sistema, posiblemente el sistema no tiene la hora correcta, recordar que Raspberry no incluye un reloj de hardware, se puede solucionar configurando la zona regional a través de raspi-config pero si eso no funciona una forma cómoda es instalando el cliente de ntp:

sudo apt-get install ntpdate

Después solo nos queda ejecutarlo indicando el servidor de tiempo que queramos usar:

sudo ntpdate time.windows.com
Docker en Raspberry Pi

Compilaciones deterministas

Una buena característica que se puede añadir a una librería además de SourceLink o crear la documentación es configurar la compilación como determinista.

Esto quiere decir que compilar ensamblados bajo las mismas condiciones de entrada producirá el mismo binario equivalente byte a byte. Por condiciones de entrada se entienden:

  • La secuencia de parámetros de entrada al compilador (flags).
  • Los contenidos del archivo de respuesta del compilador .rsp.
  • La versión exacta del compilador usado así como de los ensamblados referenciados.
  • Ruta completa del directorio actual (se pueden abreviar usando relativas)
  • Contenidos binarios de todos los ficheros pasados al compilador
    • Código fuente
    • Ensamblados referenciados
    • Módulos referenciados
    • Recursos
    • El nombre del fichero de claves (strong name)
    • Archivos de respuesta @
    • Analizadores
    • Conjuntos de reglas
    • «archivos adicionales» que puedan ser usados por analizadores
  • La cultura actual (se refiere a la cultura en la que se producen los mensajes de excepción y diagnóstico)
  • La codificación por defecto (o la actual) si la codificación no se ha especificado
  • La existencia, o no existencia, y contenidos de archivos en las rutas de búsqueda del compilador ( p.ej /lib, /recurse)
  • La plataforma CLR en la cual el compilador funciona (esto se nota por ejemplo al calcular precisiones en algunos tipos numéricos)
  • El valor de %LIBPATH% ya que esto puede afectar la carga del analizador de dependencias
  • Ahora mismo el compilador también depende de la hora del día y números aleatorios generados para GUID que no es determinístico si no se especifica /deterministic.

Permitir que el binario resultante sea el mismo proporciona varias ventajas por ejemplo para mecanismos de caché y optimización de test. También asegura que tanto la herramienta de compilación como la forma en que se ha compilado es consistente y verificable tanto por si misma con respecto al código fuente utilizado.

De igual forma en compilaciones incrementales incrementa la rapidez de compilación al compilar solo algunas partes, por ejemplo los ficheros de salida que ya están actualizados respecto a los ficheros de entrada no son ejecutados.

En el mundo DevOps proporciona ciertas ventajas para saber que pasos de compilación que dependen de cambios en un binario tienen que ser ejecutados.

La opción determinista está habilitada por defecto desde VisualStudio 2017, si se quiere habilitar para una versión anterior o deshabilitarla, tendríamos que editar el fichero .csproj.

<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

Es posible que se quiera deshabilitar ya que la opción determinista no permite establecer un número de versión autoincremental (*) por razones obvias este valor cambia en cada compilación por lo que no es determinista.

Este valor también se puede pasar al compilador a través del flag /deterministic

DevOps

Los PDBs contienen las rutas de los ficheros que se usarán para depurar, esto en entornos locales no es un problema, pero en entornos DevOps puede ser un problema al no tener acceso a esas rutas o incluso a la máquina, para esto existe la directiva <DeterministicSourcePaths/> que debe ser establecida en true, esto hace que la compilación sea completamente determinista tanto en local como en entornos DevOps.

Configuración

Con todo lo anterior lo que habría que hacer es establecer las directivas <Deterministic/> y <ContinuousIntegrationBuild/> con los valores comentados.

Además hay que incluir la directiva <EmbedUntrackedSources /> para que se incluya también el código generado por el compilador como AssemblyInfo.cs, hay que tener en cuenta que si ya está configurado para SourceLink entonces esta directiva ya estará incluida.

Además para entornos DevOps hay que añadir una configuración adicional.

Azure DevOps

<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

GitHub

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Si se está usando un target de SDK inferior a 3.1.300 hará falta agregar un fichero Directory.Build.targets con la siguiente configuración, también hará falta para librerías .NetStandard (por lo menos hasta la versión 2.1 incluida).

<Project>
  <PropertyGroup>    
    <TargetFrameworkMonikerAssemblyAttributesPath>
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 
    </TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
</Project>

Si se está usando Coverlet entonces habrá que usar la siguiente configuración para no tener problemas durante la integración continua.

<Project>
  <PropertyGroup>    <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
  <ItemGroup>
    <SourceRoot Include="$(NuGetPackageRoot)" />
  </ItemGroup>
  <Target Name="CoverletGetPathMap"
          DependsOnTargets="InitializeSourceRootMappedPaths"
          Returns="@(_LocalTopLevelSourceRoot)"
          Condition="'$(DeterministicSourcePaths)' == 'true'">
    <ItemGroup>
      <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
    </ItemGroup>
  </Target> 
</Project>

Mapear rutas

Si no se compila en formato portable es posible que las rutas dentro de los ficheros PDB no sean correctas o cambien, es posible mapear la ruta física respecto a la ruta que aparece en los PDB mediante la directiva PathMap, como:

<PropertyGroup>
  <Deterministic>true</Deterministic>
  <PathMap>$(EnlistmentRoot)=C:\</Features>
</PropertyGroup>

Siendo EnlistmentRoot la variable que apunta al raíz del código fuente, el parámetro del compilador sería:

-pathmap:path1=sourcePath1,path2=sourcePath2

Verificar

Para probarlo localmente se puede compilar pasando la propiedad TF_BUILD al compilador (si es orientado a Azure DevOps), por ejemplo:

dotnet build /p:TF_BUILD=true

Para comprobar que todo está correcto, después de compilar empaquetamos, usando por ejemplo la opción pack del proyecto y al abrirlo con NuGet Package Explorer deberíamos ver el tick verde en Deterministic.

Esto tiene una ventana añadida, podemos verificar que el paquete es correcto antes de aplicarlo a un pipe DevOps.

Referencias

Ventajas de compilaciones deterministas
Compilaciones deterministas en Roslyn
Deterministic source paths
Compilaciones incrementales
Entradas deterministas
Problemas con versionado automático
Configuración Deterministic Build
Configuración PathMap
Opción del compilador (-pathmap)
Opción del compilador (-deterministic)
Portable PDB

Compilaciones deterministas

Reflexiones Fluent Api (I): Introducción

Reconozco que utilizar una librería con Fluent Api puede ser cómodo o según como se haga bastante incómodo, como quería dar una nueva cara a una librería que tengo pensé en hacerla con estilo Fluent Api , podía ser interesante de cara a la librería y también a su facilidad de uso.

De la creación de esa librería y de algunas otras llegué a estas conclusiones que indico aquí (sobre todo para que no se me olviden la próxima vez).

Definición

La definición de Fluent Api está clara en la Wikipedia, esto no quita que haya que tener en cuenta algunos elementos o características propias del lenguaje que se vayan a usar, también indicar que se hace una distinción entre Fluent Api y method chaining, en este caso voy a usar las dos juntas indistintamente pero es importante tener esa distinción en cuenta.

También hay que tener en cuenta que el objeto con el que se está trabajando en Fluent Api es interno a la librería y puede estar compuesto a su vez de otros objetos, la idea de Fluent Api es construir ese objeto interno de una forma correcta para su uso.

Nomeclatura

Es importante tener claro como se van a prefijar los elementos y ceñirse a esta nomenclatura, muchas librerías usan los suyos propios indico aquí un estilo de ejemplo que es el que usaré:

  • AddXXX: Cuando queremos añadir un nuevo elemento al objeto interno, por ejemplo el objeto interno tiene una lista y queremos añadir un nuevo elemento a esa lista:
  • WithXXX: Para añadir una propiedad o establecer un valor al objeto con el que estamos trabajando, puede ser el interno o un objeto dependiente de este.
  • IsXXX / HasXXX: Los dos están orientado a valores booleanos, es verdad que podríamos usar también WithXXX pero muchas veces a los valores booleanos se les da un tratamiento especial para remarcarlos. Si buscásemos una diferencia entre IsXXX y HasXXX podríamos hacer lo siguiente:
    • IsXXX: Para establecer un valor booleano en una propiedad, por ejemplo IsDeveloper, IsCEO.
    • HasXXX: Cuando necesitamos hacer una verificación para obtener esa información o la sacamos de una colección, por ejemplo HasChilds, HasDriverLicense.
  • UseXXX: UseXXX se usa generalmente cuando queremos definir un comportamiento, por ejemplo internamente tenemos varias formas de crear el objeto o de guardarlo y queremos usar una en concreto, también es muy útil para establecer configuraciones.
  • AndXXX: Cuando vamos a concatenar dos propiedades que se configuran por separado pero deben de ir juntas ya que dependen entre sí.

Estilo

En este sentido también hay algunas sugerencias, por ejemplo si la línea de código es bastante larga puede ser interesante separarla en varias líneas, por ejemplo:

var configuration = Builder.Create()
                       .AddElement()
                       .WithProperty()
                       .AndAnotherOne().Save() 

El último método que finaliza se puede colocar también en otra línea aparte o en la misma, lo que interesa remarcar principalmente es cada opción de creación en una línea separada, en este punto puede haber algún problema con los editores de código de cara al formateo y la tabulación pero con un poco de práctica a la hora de generar las líneas o cambiando las opciones de formateo de código es suficiente.

Con los parámetros sucede algo parecido, es posible que los métodos tengan muchos parámetros en ese caso un nivel de indentación adicional ayuda bastante, como se ha comentado dependiendo del editor de código no siempre el formateo de código resulta cómodo así que puede haber variaciones:

var configuration = Builder.Create()
                       .AddElement(
                            text: "Hello")
                       .WithProperty(
                            description: "big",
                            color: "green")
                       .AndAnotherOne().Save() 

Por norma general se intenta usar métodos en todos los casos aunque es verdad que a veces pueda resultar más legible usar una propiedad, realmente es raro tener métodos que no usen parámetros así que por homogeneidad es mejor usar siempre métodos.

No debería pero estas configuraciones pueden llegar a ser largas por lo que incluirlas dentro de un método de configuración en una clase de inicio de la aplicación suele ser la opción más cómoda que es básicamente el comportamiento de los métodos de creación de .Net Core.

Estructura de Fluent Api

Primero recordar lo que dice la propia definición de la Wikipedia sobre la creación de las interfaces:

  • El método de una interfaz siempre devuelve como parámetro la propia interfaz.
  • Siempre hay un método sin contexto final que termina el proceso de creación (Save(), Build(),…)
  • Se trabaja siempre sobre el mismo contexto (u objeto interno) que se construirá.

Voy a usar este ejemplo demostrativo, no pretende ser un ejemplo real simplemente está hecho para poder explicar varios conceptos en los siguientes capítulos.

Diagrama general

Hay dos interfaces, una se encarga de la gestión de usuarios y la otra de la gestión de la conexión, ambas están conectadas a través de una interfaz que representa la configuración de una base de datos.

Reflexiones Fluent Api (I): Introducción

Reflexiones Fluent Api (II): Métodos

Introducción

En esta sección voy a indicar algunas notas a la hora de crear los métodos, básicamente los métodos que se van a utilizar son los que se muestran en la interfaz, su finalidad es crear el objeto.

Esquema de interfaces

Vamos ahora con algunas recomendaciones:

  • Usar parámetros solo con tipos primitivos o proporcionados por el sistema, como int, DateTime, string…, a ser posible nullables, es posible usar objetos para los métodos y en algunos casos reduce la carga de parámetros pero en ese caso estaríamos dando demasiada información sobre la creación del objeto, información que puede cambiar en un futuro y rompería la interfaz, de hecho los enumerados de usarse deberían ser propios y no los mismos que puede usar el objeto interno.
public IConnectionBuilder WithServer(
    string name = "",
    int? port = null,
    Version? version = null)
{
    Database.Server = new Server();
    Database.Server.Name = name;
    Database.Server.Port = port;
    Database.Server.Version = version;
    return this;
}
  • Todos los parámetros tienen valores por defecto, salvo los estrictamente obligatorios para una correcta creación del objeto que por otro lado idealmente deberían ser los mínimos. o no existir.
    Esto es debido a que generalmente la configuración por defecto del objeto debería ser lo más correcta posible y solo debería interesar sobrescribir algunas propiedades concretas a través de Fluent Api, acercándonos al concepto de convención sobre configuración.
    Otra opción es crear sobrecargas pero para objetos relativamente complejos la combinatoria de todos los parámetros a través de sobrecargas se hace demasiado complicada.
IConnectionBuilder WithServer(
    string name = "",
    int? port = null,
    Version? version = null);
  • Todos los parámetros están documentados, puede parecer obvio pero en Fluent Api es fácil perder el hilo de que método se está usando y sobre que objeto está siendo aplicado así que indicar el uso y efecto es importante.
/// <summary>
/// This method sets which server will be used to connect to database.
/// </summary>
/// <param name="name">Server name or IP, by default localhost.</param>
/// <param name="port">Por to connect to server, by default 669.</param>
/// <param name="version">Driver version to use, by default none.</param>
IConnectionBuilder WithServer(
    string name = "localhost",
    int? port = 669,
    Version? version = null);
  • No usar parámetros como contadores, es decir si queremos añadir 3 objetos iguales es mejor llamar tres veces al método que pasar un 3 al método, internamente a nivel de código se maneja mejor y queda más clara la intención especialmente si hay que establecer diferentes valores en estos métodos, obviamente si el valor representa internamente un contador se puede saltar esta recomendación.
IUsersBuilder builder = DatabaseBuilder
    .Create()
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
  • Una nota en particular para los métodos de extensión en C#, una ventaja que tienen es que permite crear métodos que no existan en la interfaz lo que permite esconder algunos métodos que no queremos mostrar en la interfaz o darles una implementación concreta (sí, con C# 8 se puede hacer lo mismo con la implementación de código por defecto en interfaces pero en este caso no queremos ni mostrar el método en la interfaz), de hecho se podría crear Fluent Api solo con métodos de extensión indicando en Fluent Api simplemente la herencia, parecido a como sería con mixin.
    Aunque puede parecer más cómodo no resulta muy legible ni queda claro el propósito de cada interfaz, por otro lado al ser métodos de extensión solo se puede trabajar sobre objetos estáticos y eso puede complicar el código.
    El ejemplo propuesto tiene dos interfaces para dos propósitos, configurar conexiones y configurar usuarios, imaginemos que no queremos usar interfaces y solo usar una para configurar por ejemplo los usuarios y dejar la conexión en su configuración por defecto, este sería un ejemplo donde un método de extensión puede ser útil, de cara a la interfaz siguiendo con el patrón solo existe un método sin contexto que está en la interfaz principal IDatabaseBuilder.Save() pero vía extensión hay un método Save() para cada interfaz.
public static string Save(this IUsersBuilder usersBuilder)
{
    JsonSerializerSettings settings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All };

    return JsonConvert.SerializeObject((usersBuilder as DatabaseBuilder).Database, settings);
}

Reflexiones Fluent Api (II): Métodos

Reflexiones Fluent Api (III): Interfaces

Introducción

Aunque es posible implementar Fluent Api con clases realmente la limitación de algunos lenguajes con la herencia múltiple hace que la costumbre sea usar interfaces, de hecho resulta más útil ya que es posible crear diferentes implementaciones especialmente en sistemas de configuración.

Diagrama interfaces

Lo más relevante de cara a las interfaces es:

  • El nombre indica que contiene, esto quiere decir que no importa mucho que el nombre sea un poco largo si indica que métodos va a incluir o que se puede hacer con el, hay que tener en cuenta que el nombre de la interfaz generalmente no será visible en código.
    Tampoco es inusual que el nombre interfaz se componga como una concatenación de todos las interfaces padres lo que a la hora de buscar o entender el código resulta más cómodo a falta de un diagrama a mano.
  • Crear interfaces hija al añadir objetos, si el objeto interno tiene una lista y requiere añadir objetos a esa lista lo recomendable es crear una nueva interfaz para construir objetos de esa lista interna, y el método AddXX correspondiente devolverá dicha interfaz hija para que pueda ser usada para configurar ese elemento hijo, de esta forma se proporciona métodos específicos para este objeto que no pueden ser usados por otros en las interfaces superiores.
    En el siguiente ejemplo se agrega un usuario y se devuelve una ICredentialBuilder que es específica para crear las credenciales de un usuario.
public ICredentialBuilder AddUser()
{
    login = new Login();
    logins.Add(login);

    group.Logins.Add(login);
    return this;
}
  • Las interfaces hija siempre heredan directa o indirectamente de la principal, de lo contrario nos quedaríamos bloqueados y no podríamos acceder a los métodos de la interfaz principal para continuar creando el objeto y finalmente guardarlo a través del método sin contexto.
    En el siguiente ejemplo IUserBuilder hereda de IGroupBuilder para poder añadir usuarios a un grupo, es decir que directamente no hereda de la interfaz principal IUsersBuilder pero si a través de IGroupBuilder.
public interface IUsersBuilder
{
   ...
}

public interface IUserBuilder : IGroupBuilder
{
    ICredentialBuilder AddUser();
}

public interface IGroupBuilder : IUsersBuilder
{
    IUserBuilder AddGroup(
        string name = "");
}
  • Solo la interfaz principal debe tener un método void, como indica la propia definición de Fluent Api en algún momento debemos poder indicar que el objeto está terminado y se puede construir, esté método debería estar solo en la interfaz principal para evitar crear objetos incompletos, ejemplos clásicos son Build(), Save(),…
    Está bien, es verdad que se puede usar un método de extensión para sortear esta limitación, por ejemplo guardar resultados parciales, pero a no ser que sea necesario no es lo recomendable.
public interface IDatabaseBuilder : IConnectionBuilder, IUsersBuilder
{
    string Save();
}
  • Usar el mismo fichero para crearlas, si no son muchas es más cómodo tenerlas todas juntas en un fichero que en varios separados, sobre todo porque generalmente son interfaces con pocos métodos y al haber una relación jerárquica entre ellas vía herencia es más cómodo verlas todas juntas, como nombre de fichero lo más cómodo es poner el nombre de la interfaz principal que suele ser la que mejor define el uso.
namespace FluentApi
{
    public interface IUsersBuilder
    {
        ICredentialBuilder AddLogin();

        IGroupBuilder AddGroups();
    }

    public interface ICredentialBuilder : IUsersBuilder
    {
        IRoleBuilder WithCredential(
            string username = "",
            string password = ""
            );
    }
}
  • Así como usar métodos AddXXX indica que la interfaz que devuelve servirá para crear objetos de una lista interna, es posible tener mayores niveles de anidación, por ejemplo listas de listas, esto se puede solucionar creando una interfaz que represente esta relación de creación de listas, dentro de la implementación se hará la correcta creación de las mismas.
    En el ejemplo el método AddGroups() representa este efecto, este método devuelve otra interfaz IGroupBuilder encargada de crear la lista anidada y a su vez está interfaz tiene el método que añade elementos individuales AddGroup().
public interface IUsersBuilder
{
    ICredentialBuilder AddLogin();

    IGroupBuilder AddGroups();
}
public interface IGroupBuilder : IUsersBuilder
{
    IUserBuilder AddGroup(
        string name = "");
}
public interface IUserBuilder : IGroupBuilder
{
    ICredentialBuilder AddUser();
}
  • Puede ser que tengamos dos interfaces cada una de las cuales se encargue de una parte concreta de la creación del objeto, en este ejemplo hay una interfaz para crear conexiones IConnectionBuilder y otra para crear usuarios IUsersBuilder. Si necesitásemos usar ambas necesitamos una interfaz que nos las conecte, ese es el propósito de la interfaz IDatabaseBuilder.
    Mantenemos una referencia a la interfaz principal y la usamos para configurar cada una de las partes concretas del objeto.
IDatabaseBuilder databaseBuilder = DatabaseBuilder.Create();
databaseBuilder
    .AddLogin()
    .WithCredential(
        username: "login",
        password: "password")
    .AndRole(
        database: "acme")
    .AddRight(
        permission: "read")
    .AddRight(
        permission: "write");
databaseBuilder
    .WithServer(
        name: "server",
        port: 8000,
        version: new Version("0.0.0.1"));
var json = databaseBuilder.Save();
Reflexiones Fluent Api (III): Interfaces

Reflexiones Fluent Api (IV): Clases

Introducción

Detrás de una gran interfaz siempre hay una gran clase, y en este caso no es una excepción, en general si la interfaz está bien definida en términos de herencia y parámetros de devolución no debería haber problemas.

Diagrama clases

Algunas recomendaciones:

  • El objeto interno y en general todos los que se usen deben de ser privados, esto es obvio si permitiésemos que métodos externos modifiquen el objeto interno no tendría sentido usar Fluent Api, hay que revisar todos los modificadores de acceso a todos los objetos que se usen dentro de la clase.
  • Solo puede haber un objeto interno raíz, es cierto que puede interesar tener una colección pero en ese caso es mejor tener un objeto que albergue una colección, tener un solo elemento raíz simplifica el desarrollo, por lo que en caso de querer tener una colección sería mejor crear un objeto raíz que englobe esta colección.
public class DatabaseBuilder :
    IDatabaseBuilder,
    IUserBuilder,
    IRoleBuilder,
    IRightBuilder,
    IGroupBuilder,
    ICredentialBuilder
{
 ...
}
  • Los elementos no persisten hasta llamar al método sin contexto, cualquier uso de métodos modificará el objeto en memoría y no estará disponible a través de ningún método de acceso hasta que se llame al método sin contexto (Save(), Build()…) que es el que realmente creará el objeto o lo almacenará, si fuera acceso a una base de datos el commit iría en este punto, sino, es posible emular este efecto creando objetos o listas temporales.
    En el ejemplo de muestra se usan variables privadas que almacenan partes del objeto que se usan en varias partes del método.
internal readonly Database Database = null;
private List<Right> rights = null;
private List<Login> logins = null;
private List<Group> groups = null;
private Role role = null;
private Login login = null;
private Group group = null;
  • Crear varios ficheros, en este caso daría el consejo contrario a las interfaces, a no ser que sea poco código si hay varias interfaces es recomendable tener las implementaciones en ficheros diferentes para evitar mezclarlas y que se vea más claro el uso de cada clase.
  • Vigilar la inicialización y construcción de objetos, usar Fluent Api no debería ser complicado y una buena inicialización de objetos en las clases superiores permite que estén disponibles para métodos anidados y no tengan que preocuparse de una creación condicional.
    Si el código está correctamente relacionado llamar al método final sin contexto debería ser trivial.
    Por ejemplo el método AddGroup crea no solo la lista de posibles grupos anidados sino también la lista de logins ya que al llamar a este método la única opción es llamar a métodos de la interfaz IUserBuilder que va a usar estos objetos.
public IUserBuilder AddGroup(string name = "")
{
    groups = new List<Group>();
    group = new Group();
    group.Name = name;
    logins = new List<Login>();
    group.Logins = logins;

    groups.Add(group);
    Database.Groups.Add(groups);
    return this;
}
  • No usar constructores públicos, es mejor tener un método de factoría que devuelva la interfaz principal y sea esta la que se use durante la configuración, generalmente la creación del objeto por defecto suele ser necesaria y estos detalles deberían quedar ocultos.
    En el ejemplo se usar un método estático que crea el objeto interno y devuelve la interfaz principal para poder continuar configurándolo.
private static IDatabaseBuilder databaseBuilder = null;

public static IDatabaseBuilder Create()
{
    databaseBuilder = new DatabaseBuilder();
    return databaseBuilder;
}

private DatabaseBuilder()
{
    Database = new Database();
    Database.Users = new List<Login>();
}

Reflexiones Fluent Api (IV): Clases

De mayor quiero ser programador

Leo que harán falta urgentemente 900000 desarrolladores para el 2020 -vamos que no queda nada- en una especie de suerte por salvar el mundo estilo «Stranger things».

Claro que para lograr esa alza en matriculaciones habría que acabar con muchos tópicos como que tipo de carrera es informática y quiénes sus estudiantes, si realmente sirve para algo o es mejor estudiar industriales o alguna otra carrera «de verdad», a donde nos lleva inventar formaciones no regladas sacadas de la manga… aunque de todo esto ya hablaré en otro momento.

Ahora con la vista del tiempo repaso un poco las empresas que he conocido y lo junto con lo que conozco de este mundillo para recopilar algunos de los grandes «dogmas» de la consultoría TIC -principalmente en España- tal que:

«Si quieres llegar a algo hazte jefe de proyecto, ya sabes, traje, Excel y PowerPoint» o «ya tienes una edad para seguir dándole a la tecla» etc…

Más de uno ha caído ante estos argumentos creyendo que la única vía para ser algo en esta vida pasaba por desarrollar… su cargo en la tarjeta de visita, en algunos casos es una buena opción, puede que a esa persona ya no le guste desarrollar -igual nunca le gustó- o crea que se va a hacer rico y va a trabajar lo justo -lo cual no pasa ni siempre, ni pronto- o que tener gente a su cargo es un símbolo de estatus -en plan logro desbloqueable-

A partir de aquí hay varias opciones, que sea esta una buena elección para esa persona y desempeñe su cargo con eficacia, que sea una mala elección y no hayas ganado un buen jefe de proyecto -más bien uno malo- y encima has perdido un buen desarrollador, o aún peor, ahora tienes un desarrollador que no puede dejar de serlo -porque en el fondo es lo que es y lo que le gusta- así que no hará bien su trabajo de jefe de proyecto y encima se meterá en todos los «saraos» técnicos dando su opinión -cada vez más desfasada- mientras el resto de los desarrolladores se preguntan… «¿Este de que palo va?»

Traje o no traje
¿Traje o no traje?

Ahora en plena -según algunos- «burbuja de contratación» más de un jefe de proyecto se estará preguntando aquello de a ver si pasa esto y dejan de cobrar tanto, por un lado porque es verdad que las tarifas se encarecen y los márgenes se estrechan demasiado rápido y por otro -porque no decirlo- por aquello de está gente no puede cobrar cerca del que hace las cosas importantes como yo que para eso me puse un traje, -vaya, quizás no fuera una decisión tan buena eso de ponerse un traje…-

Reconozco que de un tiempo a esta parte «huelo el miedo», hay escasez y alta rotación, resulta complicado llevar proyectos a cabo no solo porque falte gente -que ya es malo de por sí- sino porque a veces hay que resignarse a lo que quede en el mercado -que es aún peor para todas las partes-

Así que ahora todo es para hacer feliz al desarrollador, que si un futbolín, que si un puf, que si el día del orgullo friki hago un vídeo molón,… A ver si parece que soy lo que no soy y por cuatro euros engaño a algún despistado -que pasa más de lo que parece-

Vuelvo al pasado y recuerdo por ejemplo que en otros países no es así, la idea base -que es lo importante- es otra, los técnicos están bien valorados y se reconoce su importancia en el equipo como pieza clave para llevar los proyectos a buen término, de hecho se considera necesario que haya siempre alguno en las reuniones –con criterio– para que todo lo que se diga tenga una viabilidad técnica y evitar el «si a todo» -en este punto más de uno se estará emocionando-

Es por esto entre otras cosas por lo que suelo recomendar tener experiencia internacional -pero de la buena, de la de hablar en inglés y conocer gente de otros países- obviamente no todas son así pero el porcentaje de error respecto a una nacional es menor y la experiencia merece la pena.

En el ámbito nacional una empresa de base técnica, es decir, creada por, gestionada por, con mentalidad de,… «técnica» será casi siempre mejor que una gran TIC tradicional, a no ser que se quiera poner un traje en cuyo caso hay para elegir.

Me acuerdo también ahora de metodologías ágiles donde el peso de un traje queda más limitado y es el propio equipo el que se gestiona, igual por eso ahora muchas empresas prefieren invertir salarios más altos en técnicos que además de desarrollar sepan metodologías ágiles -a las empresas siempre les gusta esto de matar dos pájaros de un tiro- lo cual sutilmente provoca que el traje quede limitado a cargos más altos -alguien tiene que contar las monedas- o temas un poco más políticos -vete a comer con el cliente que yo me voy a casa-

Pongo en valor el cargo de un traje como alguna vez he defendido, pero aunque las cosas estén cambiando aunque sea por necesidad, solo el hecho de que al final un buen técnico pueda decir que lo es y que se entienda entre el gran público que es un cargo con valor equiparable en responsabilidad y estatus a prácticamente cualquier cargo de gestión es algo que hará que mucha gente quiera subirse a este barco y no bajarse y esto es lo importante, es lo que hará que realmente cambie el escenario por encima de noticias de empleo y ascensos aspiracionales que quizás realmente no están solucionando el problema sino que lo están agravando.

Reconoceré que no me gusta que ser técnico sea un cargo de transición hacia algo mejor, que tenga que haber limitaciones en responsabilidad o sueldo, que tenga que elegir entre vocación o carrera y mucho menos que tenga que escuchar con gesto de sorpresa -¿Pero no quieres hacer otra cosa?- entiéndase por cosa llevar traje, cobrar más, hacer algo cool y bobadas varias.

Puede que incluso se me diera bien llevar un traje -mas de una vez me lo han dejado caer- pero para mi al contrario de lo que pueda parecer sería un retroceso, sería reconocer que ya no se me da bien lo que hago, que no puedo aportar más y que en cierta forma me estoy jubilando y reconociendo que solo me quedará la nostalgia y las frases de antes lo hacíamos así y acabábamos antes, me gusta resignarme a esta idea aunque al final tenga que claudicar porque la tecnología me supere y seguir pensando que, de mayor quiero ser programador.

De mayor quiero ser programador

Configurando SourceLink

Cuando se crean aplicaciones es muy habitual usar paquetes NuGet, muchas veces estos paquetes son de código libre y están albergados en GitHub o puede que sean privados y estén depositados en servidores NuGet privados.

Cuando se está desarrollando una aplicación a veces es cómodo poder depurar paquetes NuGet para obtener información como si fuera código local, para esto sirve SourceLink para permitir que el desarrollador pueda acceder al código fuente de un paquete NuGet.

Para que esto funcione el paquete debe soportar SourceLink así que es posible que no todos los paquetes que existan actualmente puedan usar este método.

También hará falta una versión de Visual Studio 15.3 y se recomienda una 15.7 para obtener toda la funcionalidad.

En este ejemplo se va a utilizar .Net Core que viene preparado para SourceLink, aunque se soportan otros formatos como .Net Framework así como varios formatos de PDB (Portable, Embedded y Windows PDBs).

Configuración

Es recomendable que la información de depuración de los proyectos sea portable, otros formatos son dependientes de Windows y no siempre funcionan bien.

Editamos el archivo .csproj y añadimos la siguiente información que es la recomendada por SourceLink.

<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
 
    <!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 
    <!-- Optional: Embed source files that are not tracked by the source control manager in the PDB -->
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
  
    <!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
  <ItemGroup>
    <!-- Add PackageReference specific for your source control provider (see below) --> 
  </ItemGroup>
</Project>
  • PublishRepositoryUrl: Indicamos que queremos que en la información del paquete aparezca la Url del repositorio.
  • EmbedUntrackedSources: Permite añadir cómo código fuente a usar durante la depuración código que no esté asociado al repositorio de este paquete o que esté fuera del control del código fuente (como un AssemblyAttribute.cs), hay que tener cuidado porque puede añadir mucho código que igual no nos interesa.
  • IncludeSymbols: Construye el paquete .snupkg asociado que contiene la información necesaria para la depuración, otra forma sería añadirlo al paquete principal pero esto aumentaría el tamaño.
  • SymbolPackageFormat: Formato del paquete de símbolos.

Si tenemos problemas a la hora de depurar por ejemplo al tener las librerías en un repositorio privado podemos probar a usar está configuración que nos incluirá los símbolos dentro de la librería en lugar de un paquete de símbolos aparte, para Nuget.org se recomienda crear un paquete aparte (*.snupkg) como se indica aparte, en ese caso se puede omitir la directiva SymbolPackageFormat.

<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

La configuración que hemos establecido hasta ahora lo que hace es crear una etiqueta <repository> con la dirección del repositorio así como el commit asociado a esta versión de código.
A través de Nuget Package Explorer se vería así.

<repository>

Varios paquetes por proyecto

En el caso de que queramos usar el fichero .nuspec, por ejemplo si tenemos varios proyectos y queremos crear un solo paquete con varios, tenemos que añadir esta misma etiqueta manualmente y parametrizarla ya que dotnet pack no funciona igual cuando se usa .nuspec, así que dentro del .nuspec añadimos:

<repository type="$RepositoryType$" url="$RepositoryUrl$" branch="$RepositoryBranch$" commit="$SourceRevisionId$"/>

Y los parámetros los tenemos que referenciar a través del .csproj añadiendo esta etiqueta.

<NuspecProperties>version=$(PackageVersion);SourceRevisionId=$(SourceRevisionId);RepositoryUrl=$(RepositoryUrl);RepositoryType=$(RepositoryType);RepositoryBranch=$(RepositoryBranch)</NuspecProperties>

Si usasemos un comando como el siguiente se rellenarían los valores correctos dentro de la etiqueta <repository>, más adelante se indicará como hacerlo a través de Azure DevOps.

dotnet pack .\Folder\Project.csproj -p:NuspecFile=C:\Projects\Project\Folder\File.nuspec -p:PackageVersion=4.9 -p:SourceRevisionId=7aab58c9134r2146dfr595a335e1474f4861649c -p:RepositoryUrl=https://github.com/acme/coyote -p:RepositoryType=git -p:RepositoryBranch=develop -o C:\folder\packages

También hay que tener en cuenta que si se quieren añadir los símbolos de depuración dentro del paquete hay que usar la siguiente etiquetas en lugar de <IncludeSymbols> ya que si existen ambas no se guardarán los .pdb dentro del paquete.

<DebugSymbols>true</DebugSymbols>

Ahora vamos a añadir los paquetes que harán falta para habilitar la depuración, este paquete lo marcamos como PrivateAssets=»All» (normalmente no hará falta hacerlo), esto evita que los proyectos que usen este paquete intente también usar el propio paquete SourceLink,

Los paquetes a instalar dependen del repositorio donde esté el código, unos ejemplos serían:

GitHub

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
</ItemGroup>

Aquí incluimos la referencia a paquete que antes hemos dejado vacía, actualmente la librería se encuentra en prerelease así que tendremos que marcar el check de NuGet Include prerelease para poder descargarla.

Azure DevOps

En este caso la referencia a añadir sería esta, al igual que para GitHub esta librería está en modo prerelease.

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.Vsts.Git" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
</ItemGroup>

Otros

Existen otras alternativas (GitLab, TFS, Bitbucket, …) que se pueden consultar en la propia web.

Sino aparece el paquete exacto para el repositorio la opción es usar Microsoft.SourceLink.GitHub.

Puede suceder que un paquete referencie a otros paquetes de otros proveedores, por ejemplo un paquete de GitHub que referencia a uno de Azure DevOps, en este caso la aplicación debe incluir las referencias a los paquetes necesarios a todos ellos.

Verificando

Para estar seguro de que el paquete corresponde a la versión podemos verificar que el commit que aparece en el paquete es el adecuado usando el comando git log.

Para comprobar que todo está bien configurado podemos usar la herramienta SourceLink, ejecutando este comando desde una consola de desarrollador o desde el propio VS podremos instalarla de forma global.

dotnet tool install --global SourceLink --version 3.0.0 
Instalando SourceLink como herramienta global

Ahora podemos lanzar comandos, por ejemplo el mapeo del código para ver si el .json está dentro de los .pdb con:

sourcelink print-json paquete\nCubed.EFCore.pdb
print-json

O comprobar que los ficheros se pueden descargar con:

sourcelink test paquete\nCubed.EFCore.pdb

O para imprimir el código SHA para ver los ficheros que contiene el .pdb:

sourcelink print-documents paquete\nCubed.EFCore.pdb
print-documents

O incluso las URL donde se ubicarán los ficheros de código fuente, estas direcciones son reales y se debería poder ver el contenido del código:

sourcelink print-urls paquete\nCubed.EFCore.pdb
print-urls

Publicando

Si queremos crear el paquete usando el pipeline de compilación de Azure a través de una tarea dotnet tendremos que usar el comando personalizado y no el pack (la opción de especificar un fichero .nuspec en lugar de .csproj on parece funcionar), dentro de los parámetros podemos usar variables de entorno para rellenar los valores necesarios para .nuspec. En este caso la versión es una variable que va dentro del pipeline, no es de entorno.

De cara a la publicación de los paquetes será necesario una versión del cliente NuGet al menos 4.9.0 y usar la URL v3 para poder publicar los símbolos, por ejemplo para NuGet.org https://api.nuget.org/v3/index.json

Puede pasar un tiempo desde que se publican los símbolos en GitHub hasta que los símbolos estén disponibles (máximo 1 hora pero suele ser menos).

Si tenemos un error como:

C:\Windows\ServiceProfiles\NetworkService\.nuget\packages\sourcelink.create.commandline\2.8.3\build\SourceLink.Create.CommandLine.targets(30,5): error : unable to convert OriginUrl: https://{private}.visualstudio.com/dotNET%20Project/_git/company.cqrslite [C:\agent\_work\34\s\company.CQRSlite\company.CQRSlite.csproj]

En ese caso tenemos que eliminar este paquete SourceLink.Create.CommandLine

Consejo DevOps, es mejor publicar de forma separada el paquete y los símbolos, o al menos tener la opción, ya que por ejemplo NuGet.org no permite publicar la misma versión de nuevo lo que impediría subir los símbolos al fallar la tarea anterior.

Visual Studio

Solo queda un detalle final, deshabilitar la opción Just My Code en las opciones de Visual Studio Tools->Options->Debugging esto permite poder acceder a otros orígenes de código y no solo al propio del desarrollador.

Deshabilitando Just My Code

Si se esta depurando código fuente que ya existe localmente en la máquina sera ese el que se use, pero si el código no existe en la máquina (que suele ser lo normal) se mostrará un mensaje como el siguiente.

Aviso de advertencia antes de descargar código

Eligiendo la primera opción se descargará el código en una ruta como:

C:\Users\user\AppData\Local\SourceServer\ 1c6a834f2006f59a0cb6dd7101918efae8ad05b614b5fc36d11bfc9c6d9bcb9b \src\proyecto\carpeta\codigo.cs

Este tipo de rutas permite que se puedan bajar diferentes versiones del código para diferentes versiones del mismo.

Referencias

https://stackoverflow.com/questions/53485362/pass-variable-to-nuspec-with-dotnet-pack
https://docs.microsoft.com/es-es/nuget/reference/nuspec#replacement-tokens
https://cezarypiatek.github.io/post/setting-assembly-and-package-metadata/
https://devblogs.microsoft.com/nuget/introducing-source-code-link-for-nuget-packages/
https://github.com/ctaggart/SourceLink/issues/256

Configurando SourceLink

Básicos Git (V): Amend

El propósito de Amend lo que hace es agrupar commits antes de hacer un push, si por ejemplo se está desarrollando una funcionalidad y nos damos cuenta que hemos olvidado añadir algo en lugar de crear dos commits separados podemos crear uno y después hacer un amend para agruparlos.

Imaginemos que tenemos un solo cambio en ese caso veríamos lo siguiente:

un solo cambio pendiente

Si hacemos esto varias veces tendremos cada vez más commits:

varios commits pendientes

Así que para evitar acumular commits a la hora de crear uno nuevo seleccionamos la opción ammend previous commit.

uso de ammend

Esto añadirá los cambios al anterior commit manteniendo solo uno con todas las modificaciones, el comentario de este nuevo commit será el mismo que tenía el commit anterior sino especificamos uno nuevo.

Básicos Git (V): Amend

Básicos Git (IV): Reset

Reset hace que el apuntador actual de la rama se desplace pudiendo apuntar a commits anteriores o posteriores en el historial.

Normalmente el apuntador de la rama apunta al último commit pero es posible moverlo en caso de que se quiera volver a una situación anterior de la rama y continuar a partir de ella.

Vamos a suponer que tenemos inicialmente una rama master y una develop, al crear la rama develop como es normal el apuntador apunta a HEAD, es decir al último commit, si añadiéramos un nuevo commit (Commit #3) el apuntador se desplazaría junto con el.

inicial

Ahora vamos a usar la opción reset para desplazarnos hacia atrás en el historial, para esto accederemos al historial de main y usaremos la opción reset estando posicionados en la rama en la que nos queremos mover, en este caso develop.

Keep changes (–mixed)

Esta opción provoca que si tenemos ficheros modificados se mantendrán a la par que nos desplazamos hacia atrás en el historial.

reset –mixed

Para verificar esto volvemos a la sección changes y veremos que los cambios siguen ahí y que a su vez los demás ficheros han sido cambiados a la versión concreta elegida.

Keep changes (–hard)

Esta opción provoca que si tenemos algún cambio se perderá a la par que nos desplazamos hacia atrás en el historial.

reset –hard

Para verificar esto volvemos a la sección changes y veremos que no hay ningún cambio pendiente y al igual que en el caso anterior los ficheros han sido cambiados a la versión elegida.

Bonus

Se puede deshacer el último commit usando reset en una rama local, simplemente habría que hacer un reset al commit anterior al que estamos.

Esto se debe a que como no hemos hecho push no hemos avanzado en el historial y al hacer reset podemos volver al punto de partida.

Por ejemplo tenemos un commit en una rama local:

historial de la rama

Ahora vamos a deshacer el último, tenemos que seleccionar el anterior (Commit Id e52d15a7).

detalle del commit

Solo tenemos que pulsar la opción de reset eligiendo –hard si queremos deshacer el commit y borrar el contenido o –mixed si queremos deshacer el commit pero manteniendo el contenido, en este caso veremos los contenido del commit en la sección de changes.

contenido del commit usando –mixed

Como detalle final se puede hacer también un reset al último commit de la rama local, en ese caso lo que haríamos sería ponerlo como commit pendiente de realizar un push, es decir seguiría siendo un commit y no aparecía en changes.

Esto nos puede servir para modificarlo a través de un amend si queremos mantener el contenido pero además agregar otros nuevos.

Básicos Git (IV): Reset