Fossil-scm
Ya comenté que estaba probando Fossil-scm para ver si merecía la pena el cambio después de los esfuerzos que he hecho para comprender git-scm, el que nunca parece que llegaré a dominar me esfuerce lo que me esfuerce. Algunos me habéis preguntado si Fossil no se aleja demasiado de la filosofía Unix que tanto me gusta y es cierto, lo hace, pero estoy dispuesto a concederle una excepción en mis preferencias y otros, me habéis hecho otras preguntas. En este artículo, quiero contar mis experiencias de uso de la herramienta, tras más de un año utilizándolo y ya no en pruebas, además de contestar algunas preguntas sobre ramas y forks que surgieron a raíz de otro artículo anterior donde contaba cómo utilizarlo desde Emacs1. Por tanto, dejadme que el formato sea el de preguntas y respuestas.
¿Por qué te gusta Fossil si se aleja tanto de la filosofía Unix?
Sí, es cierto. Fossil te proporciona muchas más herramientas que el simple seguimiento del código de un proyecto; una web de repositorio donde puedes encontrar multitud de herramientas:
- Un wiki para la documentación
- Foro para intercambiar opiniones
- Gestión de tickets o tareas, que se pueden abrir, adjudicar, priorizar, modificar y cerrar cuando estén cumplidas.
- Herramientas de administración del repositorio.
- Chat para intercambiar opiniones en directo.
- Documentación de ayuda de Fossil.
Proporciona, por defecto, todo lo que proporcionan páginas como github
o gitlab (entre las privativas), o gitea, o codeberg. Lo tienes todo
dentro de un único fichero, que es una base de datos sqlite3
y te lo
puedes llevar donde quieras. Puedes usarlo en un servidor o como
repositorio local, o ambas cosas. Utilizo, además, Nextcloud para
que me haga de remoto con los repositorios. Cuando utilizaba
repositorios git, la actualización de Nextcloud solía lanzarme
errores, tenía que asegurarme que los directorios ocultos se
actualizaban correctamente. Con Fossil no existe ese problema.
¿Cómo creo un repositorio nuevo?
El proceso es sencillo. Voy a crear un proyecto nuevo para hacer
pruebas con él y no estropear ninguno de los míos. En mi caso, tengo
agrupados los repositorios en ~/Nextcloud/repos
y los locales donde
trabajo en ~/proyectos
. Los pasos son los siguientes:
Crear el fichero que hará de repositorio.
fossil init ~/Nextcloud/repos/prueba-fossil.fossil
Nos contestará algo parecido a:
≻ fossil init ~/Nextcloud/repos/prueba-fossil.fossil project-id: 97a74719ac63a1231e43a2c5c15fbc39416fa807 server-id: 9f581b18342998e2d839e559c954357b97876cd4 admin-user: notxor (initial password is "FDrW4pztN6")
Podemos observar que crea un usuario administrador con el nombre de nuestro usuario y establece una contraseña para el mismo. La contraseña no la pide cuando trabajes en local, pero si el repo se accede a través de servidor será necesaria.
Crear el directorio de trabajo y entrar en él:
cd ~/proyectos mkdir prueba-fossil cd ~/proyectos/prueba-fossil
Abrir el repositorio de trabajo se hace utilizando el comando
open
en ese directorio. Nos contará algo como lo siguiente:≻ fossil open ~/Nextcloud/repos/prueba-fossil.fossil project-name: <unnamed> repository: /home/notxor/Nextcloud/repos/prueba-fossil.fossil local-root: /home/notxor/proyectos/prueba-fossil/ config-db: /home/notxor/.config/fossil.db project-code: 97a74719ac63a1231e43a2c5c15fbc39416fa807 checkout: 7d1e11d43506a1c32815d181bd1e7a3a09f6eb15 2022-06-25 09:40:22 UTC tags: trunk comment: initial empty check-in (user: notxor) check-ins: 1
A partir de aquí ya podemos trabajar en el contenido y sincronizar el trabajo con nuestro repositorio remoto, que en mi caso, gracias a Nextcloud, también es local pero guarda copia en otra máquina remota.
¿Cómo descargo un repositorio a mi disco duro?
Esta pregunta tiene dos acepciones en Fossil, una es descargarte el
archivo contenedor y otra es abrir un repositorio local donde hacer
cambios, compilar el código, etc. Hemos visto ya, que para abrir un
repositorio local de trabajo tenemos que hacer el paso del punto 3 de
la pregunta anterior. Así que me centraré en obtener un fichero
.fossil
de un servidor remoto.
Para las pruebas voy a necesitar un servidor. Voy a montar uno con mis
repos, que los guardo, como ya he dicho, en ~/Nextcloud/repos
.
Levantar un servidor local es tan sencillo como utilizar el comando
server
de Fossil:
> fossil server ~/Nextcloud/repos --port 8080 Listening for HTTP requests on TCP port 8080
A partir de ahí están disponibles todos los repositorios que haya en
ese directorio, en ficheros con extensión .fossil
, pero recuerda que
son bases de datos SQLite3. Para conectarnos al servidor del
repositorio, podemos utilizar cualquier navegador:
Podemos apreciar que se apunta la URL
a localhost
y se añade el
nombre del repositorio sin la extensión .fossil
. Para descargar el
repositorio completo clonándolo en un directorio local.
≻ fossil clone http://localhost:8080/prueba-fossil prueba-fossil.fossil
Round-trips: 2 Artifacts sent: 0 received: 4
Clone done, wire bytes sent: 544 received: 1327 ip: 127.0.0.1
Rebuilding repository meta-data...
100.0% complete...
Extra delta compression...
Vacuuming the database...
project-id: 97a74719ac63a1231e43a2c5c15fbc39416fa807
server-id: 3a8847c1dea9b851e362ebbbe689c11257344c50
admin-user: notxor (password is "qY7GseCMph")
Vemos que nos genera un nuevo fichero .fossil
, con el nombre de
nuestro usuario (en mi caso notxor
) y nos proporciona una contraseña
para conectarnos con este repositorio, que es distinta a la contraseña
del repositorio remoto.
Suele ser más sencillo hacerlo en un único paso si hacemos desde el
servidor un open
en lugar de un clone. Para hacerlo desde cero he
borrado el directorio de trabajo y lo he vuelto a crear vacío. Luego
he hecho el siguiente comando:
≻ fossil open http://localhost:8080/prueba-fossil fossil clone http://localhost:8080/prueba-fossil /home/notxor/proyectos/prueba-fossil/prueba-fossil.fossil Round-trips: 2 Artifacts sent: 0 received: 4 Clone done, wire bytes sent: 550 received: 1327 ip: 127.0.0.1 Rebuilding repository meta-data... 100.0% complete... Extra delta compression... Vacuuming the database... project-id: 97a74719ac63a1231e43a2c5c15fbc39416fa807 server-id: f160e27ad3a030c343c86d842d8e2abf0400a389 admin-user: notxor (password is "UtWc3CDZgA") Pull from http://localhost:8080/prueba-fossil Round-trips: 1 Artifacts sent: 0 received: 0 Pull done, wire bytes sent: 321 received: 359 ip: 127.0.0.1 project-name: <unnamed> repository: /home/notxor/proyectos/prueba-fossil/prueba-fossil.fossil local-root: /home/notxor/proyectos/prueba-fossil/ config-db: /home/notxor/.config/fossil.db project-code: 97a74719ac63a1231e43a2c5c15fbc39416fa807 checkout: 7d1e11d43506a1c32815d181bd1e7a3a09f6eb15 2022-06-25 09:40:22 UTC tags: trunk comment: initial empty check-in (user: notxor) check-ins: 1
No me gusta tener el .fossil
local en el mismo directorio que el
código y como digo, suelo utilizar repositorios que son alcanzables de
manera local. Sin embargo, cuando he clonado un repositorio a través
de red recomiendo los siguientes pasos:
Crear el directorio del proyecto:
≻ cd proyectos ≻ mdkir proyecto-nuevo ≻ cd proyecto-nuevo
Clonar el repositorio:
≻ fossil clone https://usuario:password@sitio-repo/nuevo-proyecto ../nuevo-proyecto.fossil
Fíjate que paso el usuario y la contraseña. También se puede emplear
ssh
, por ejemplo. En todo caso, el.fossil
clonado prefiero que esté en el directorio padre (en mi caso~/proyectos
) y no donde el código fuente.Abrir el repositorio clonado:
≻ fossil open ../nuevo-proyecto.fossil
Si todo ha ido bien, cada cambio que hagamos en ese repositorio se
asignará al usuario que hayamos utilizado en la URL del paso 2.
También nos preguntará si queremos que recuerde la contraseña, si le
decimos que sí la recordará y la sincronización será más rápida. Si
tenemos activado auto-sync
en el repositorio, cuando hagamos cambios
en el local, se subirán al remoto sin necesidad de hacer push
(particularmente, prefiero ser yo quien decida cuándo modifico el
repo remoto y lo tengo desactivado en todos mis repos).
¿Por qué fossil
no me muestra los archivos nuevos?
Si estás acostumbrado a otros sistemas, es posible que al preguntar con
fossil status
cuál es el estado del repositorio, te sorprenda que no
muestre archivos nuevos. Crearé uno en nuestro repositorio de pruebas
para que se vea correctamente el asunto:
echo "Esto es un repositorio de pruebas para mostrar cómo funciona `fossil-scm`." > README.md
En nuestro repositorio estarán los siguientes ficheros:
≻ ls prueba-fossil.fossil README.md
Vemos claramente el archivo clonado y nuestro nuevo README.md
, sin
embargo, al mirar el estado del repositorio, nos aparece lo siguiente:
≻ fossil status repository: /home/notxor/proyectos/prueba-fossil/prueba-fossil.fossil local-root: /home/notxor/proyectos/prueba-fossil/ config-db: /home/notxor/.config/fossil.db checkout: 7d1e11d43506a1c32815d181bd1e7a3a09f6eb15 2022-06-25 09:40:22 UTC tags: trunk comment: initial empty check-in (user: notxor)
No se ha enterado de nuestro nuevo archivo. Para que el repositorio lo
tenga en cuenta, debemos añadir la opción --extra
al comando:
≻ fossil status --extra repository: /home/notxor/proyectos/prueba-fossil/prueba-fossil.fossil local-root: /home/notxor/proyectos/prueba-fossil/ config-db: /home/notxor/.config/fossil.db checkout: 7d1e11d43506a1c32815d181bd1e7a3a09f6eb15 2022-06-25 09:40:22 UTC tags: trunk comment: initial empty check-in (user: notxor) EXTRA README.md
Está marcado como EXTRA
, es decir, no pertenece al repositorio. Si
queremos que fossil
lo tenga en cuenta, debemos añadirlo al mismo.
≻ fossil add README.md ADDED README.md ≻ fossil status repository: /home/notxor/proyectos/prueba-fossil/prueba-fossil.fossil local-root: /home/notxor/proyectos/prueba-fossil/ config-db: /home/notxor/.config/fossil.db checkout: 7d1e11d43506a1c32815d181bd1e7a3a09f6eb15 2022-06-25 09:40:22 UTC tags: trunk comment: initial empty check-in (user: notxor) ADDED README.md
Para completar el proceso, vamos a hacer un commit
:
≻ fossil commit -m "Añadido fichero README.md" Pull from http://localhost:8080/prueba-fossil Round-trips: 1 Artifacts sent: 0 received: 0 Pull done, wire bytes sent: 393 received: 360 ip: 127.0.0.1 New_Version: bf6f949de739c5965d80ba8e2ad9b67a412442d7d73766f66239fbee03be9c6b Sync with http://localhost:8080/prueba-fossil Round-trips: 1 Artifacts sent: 2 received: 0 Sync done, wire bytes sent: 670 received: 356 ip: 127.0.0.1 Warning: The check-in was successful and is saved locally but you are not authorized to push the changes back to the server at http://localhost:8080/prueba-fossil
Lo primero que hace es un pull
del repositorio remoto. Pero llama
más la atención el warning
final. Para sincronizar el repositorio
local con el remoto necesitamos hacerlo con usuario y contraseña:
≻ fossil sync http://localhost:8080/prueba-fossil -B notxor:Mi-Contraseña Round-trips: 1 Artifacts sent: 0 received: 0 Sync done, wire bytes sent: 529 received: 397 ip: 127.0.0.1
No entiendo cómo se gestionan las ramas
Vamos a imaginar que necesitamos dos ramas de desarrollo para nuestro magnífico proyecto de pruebas y vamos a crear dos ramas:
≻ fossil branch new Primera trunk ≻ fossil branch new Segunda trunk
Ahora, si hacemos un listado con las ramas del repositorio nos dirá lo siguiente:
≻ fossil branch ls Primera Segunda * trunk
El listado de ramas muestra tres ramas: trunk
, marcada con un *
es
la rama activa de nuestro repo. En la timeline gráfica se puede
apreciar lo siguiente:
Otra curiosidad de Fossil es que, como se puede ver en la imagen, al
final de cada rama, el último commit
es el leaf
de la misma.
Vamos a hacer cambios en una rama, por ejemplo, en Primera
y
seguimos el siguiente procedimiento:
Cambiar a la rama
Primera
:≻ fossil branch ls Primera Segunda * trunk ≻ fossil update Primera ------------------------------------------------------------------------------- checkout: 13f6106d312e9aa54ae326f0576761287d31a527 2022-06-25 12:02:22 UTC tags: Primera comment: Create new branch named "Primera" (user: notxor) changes: None. Already up-to-date ≻ fossil branch ls * Primera Segunda trunk
Tras hacer unos cambios en el único archivo que tenemos y subir esos cambios, en cada una de esas ramas tenemos un gráfico similar a:
Llevar los cambios a trunk
no es complicado. Recordemos que en
Fossil no existe el rebase
, sólo el merge
.
≻ fossil update trunk UPDATE README.md ------------------------------------------------------------------------------- updated-from: 3d3eede7e32fd6e3024e41cd8f61bccae3842597 2022-06-25 13:46:08 UTC updated-to: a359332a2a5961dec66f59142d762d3d137dd8a2 2022-06-25 12:01:45 UTC tags: trunk comment: Añadido fichero README.md (user: notxor) changes: 1 file modified. "fossil undo" is available to undo changes to the working checkout. ≻ fossil merge Primera UPDATE README.md "fossil undo" is available to undo changes to the working checkout. ≻ fossil commit -m "Commit para el merge con la rama Primera." New_Version: d76eb3b89ad477536069e218cbdbc075c25c043d62ca28a53942ac05780ed05f
Los pasos han sido:
- Cambiar a la rama
trunk
- Hacer
merge
con la ramaPrimera
- Hacer un
commit
con los cambios aportados por elmerge
.
El gráfico queda así:
≻ fossil merge Segunda MERGE README.md ***** 1 merge conflict in README.md WARNING: 1 merge conflicts "fossil undo" is available to undo changes to the working checkout. ≻ emacs -nw README.md
Al hacer el merge
con la segunda rama, como era de esperar, hay
conflictos pues sólo hay un archivo y con poco contenido que separe
suficientemente los cambios para que no entren en conflicto. El
aspecto del fichero README.md
, al editarlo, es el siguiente:
Una vez hechos los cambios lo dejamos así:
Esto es un repositorio de pruebas para mostrar cómo funciona `fossil-scm`. Esto es un listado con cambios: + Cambios en la rama `Primera` + Es un cambio hecho desde la `Segunda` rama.
Una vez solucionados los conflicto, ya podemos hacer el commit
y
nuestro árbol quedará así:
Hay que recordar que los cambios en las dos ramas se han pasado a
trunk
, pero si volvemos a una de las ramas:
≻ fossil update Primera UPDATE README.md ------------------------------------------------------------------------------- updated-from: e09a941c5d43eb1faa333db47a0c87bcb8b001f7 2022-06-25 14:13:54 UTC updated-to: a4838e1179459d57804d159ef0155d0c54d47118 2022-06-25 12:07:43 UTC tags: Primera comment: Cambios para el ejemplo en la rama Primera. (user: notxor) changes: 1 file modified. "fossil undo" is available to undo changes to the working checkout. ≻ fossil merge trunk UPDATE README.md "fossil undo" is available to undo changes to the working checkout.
Al cambiar a la rama Primera
lo primer que nos advierte con UPDATE
README.es
es que debemos actualizar porque ha habido cambios que no
están en esta rama. Hacemos un merge
con trunk
y debería estar ya
toda la información cargada en esta rama también, también podríamos
haber hecho un update
. Te recuerdo que no hay rebase
en Fossil
pero se pueden hacer los merges
y updates
que sean necesarios. Voy
también a hacerlo en la otra rama:
≻ fossil update Segunda MERGE README.md ------------------------------------------------------------------------------- updated-from: a4838e1179459d57804d159ef0155d0c54d47118 2022-06-25 12:07:43 UTC updated-to: 3d3eede7e32fd6e3024e41cd8f61bccae3842597 2022-06-25 13:46:08 UTC tags: Segunda comment: Cambios para el ejemplo en la rama Segunda. (user: notxor) changes: 1 file modified. uncommitted merge against e09a941c5d. WARNING: 1 uncommitted prior merges "fossil undo" is available to undo changes to the working checkout.
En este caso, como vemos, fossil
pide un merge
. Cuando fossil
pide update
, normalmente no habrá conflicto, pero si pide merge
sí. No quiero liar mucho la perdiz, sigamos sin más conflictos, por el
momento.
Volvemos a la primera rama, la que llamamos Primera
. Después de
hacer el merge
con trunk
lo suyo sería hacer un commit
para
sincronizar el repositorio y la copia de trabajo:
≻ fossil branch ls * Primera Segunda trunk ≻ fossil commit -m "Merge con la rama principal." New_Version: 5519680712d91b66d2ce32121ded84c47e10a7869c80b5d2da40f3587e2c987a
¿Cómo se ve gráficamente? Te puedes estar preguntando, así:
Si nos fijamos en el gráfico, es posible liarse con tanta flechita,
sin embargo, ya me he acostumbrado a ello e interpreto las líneas más
finas como el camino que llevan los cambios de una rama a otra. Es
decir, ese gráfico lo interpreto como que los cambios han ido de las
ramas Primera
y Segunda
a la rama trunk
. Luego, desde trunk
,
los cambios (los de la rama Segunda
) pasan a la rama Primera
.
Forks
No hay mucha diferencia entre lo que en Fossil se llama fork
y lo
que es una rama (branch
) ¿Cuál es la diferencia?, pues que una rama
es un fork
hecho de manera voluntaria. Cualquier división o
ramificación en el repositorio es una rama o un fork
según cómo lo
mires o lo quieras llamar. Por tanto, existen ramas creadas de manera
accidental... ¿cómo?
Imagina que dos usuarios del repositorio están modificando el mismo fichero de manera independiente. El primero en terminar su trabajo, lo subirá al repositorio. El segundo sigue trabajando y cuando termina sube los cambios al repositorio. En ese momento, Fossil le avisará de que hay un conflicto. Ha habido cambios desde que actualizó por última vez el repositorio:
- Si subiera sus cambios, machacaría el trabajo del primero.
- Si hace un
pull
del repositorio para actualizarse, puede perder su trabajo.
Si opta por subir sus cambios forzándolo con --force
, lo que hace
Fossil es crear un fork
que no machacará el trabajo de nadie.
Después se hará un merge
o lo que sea necesario, para actualizar
todos los cambios. Si se opta por actualizar sin hacer un fork
lo
habitual es hacer:
fossil upgrade -n -v
La opción -n
hace lo que llaman un dry-run
. Básicamente hará un
update
de los ficheros donde no haya conflictos y un merge
de los
que entren en conflicto. Hecho eso se modifican los archivos
conflictivos para arreglarlos y luego se hará el commit
sin
problemas.
¿Cómo puedo convertir un repositorio de git
a fossil
?
Tanto git
como fossil
tienen comandos para importar y exportar los
datos de un repositorio. Hay modos de pasar un repositorio de una
herramienta a otra. Recuerda que en el de Fossil también hay
entradas para marcar cuándo se ha modificado una página del wiki o
se ha creado un ticket, o se ha cambiado su prioridad, o se ha
completado, o... Git no tiene dichos chismáticos así que esas
partes no se pueden importar ni exportar. Para eso utilizaremos la
opción --git
indicándole a fossil
que se ciña a lo que entiende
dicha herramienta.
Para importar a Fossil un repositorio desde Git, se emplea el siguiente método:
cd git-repo git fast-export --all | fossil import --git nuevo-repo.fossil
con ese par de comandos, vale son tres, pero dos están enlazados
con un pipe
para usar la salida de uno como la entrada del otro,
obtenemos el fichero nuevo-repo.fossil
. Una vez obtenido lo podemos
ya clonar, abrir o lo que queramos hacer con él.
Para hacerlo al contrario. Es decir, pasar un repositorio de Fossil a Git, los pasos son similares pero inversos:
git init nuevo-repo cd nuevo-repo fossil export --git /camino/al/repo.fossil | git fast-import
Como se puede apreciar, aquí la diferencia es que primero hay que
crear el directorio del nuevo repositorio para git
y luego se le
pide a fossil
la información estando en el directorio que queremos
utilizar como repositorio.
¿Se puede tener un repositorio fossil
y actualizar un git
?
La respuesta corta es no. Pero puedes sincronizar un par de repositorios, uno Git y otro Fossil. Se puede, según la documentación de Fossil, pero yo nunca lo he conseguido hacer sin obtener algún error bloqueante. Lo mismo tú tienes más suerte que yo (o no eres tan patazas).
Imagina que tienes un repo remoto en Fossil que quieres sincronizar con un repo en Git. La documentación afirma que habría que hacer algo así (lo copio literalmente, para no cagarla):
fossil clone /path/to/remote/repo.fossil repo.fossil mkdir repo cd repo fossil open ../repo.fossil mkdir ../repo.git cd ../repo.git git init . fossil export --git --export-marks ../repo/fossil.marks \ ../repo.fossil | git fast-import \ --export-marks=../repo/git.marks
Una vez completada la descarga se necesitaría actualizar con git
checkout trunk
. En todo caso, hecho esto, se supone que los cambios
en el repositorio Fossil se pueden importar al repositorio Git con
la siguiente secuencia:
cd ../repo fossil pull cd ../repo.git fossil export --git --import-marks ../repo/fossil.marks \ --export-marks ../repo/fossil.marks \ ../repo.fossil | git fast-import \ --import-marks=../repo/git.marks \ --export-marks=../repo/git.marks
Al contrario, los cambios en el repositorio de Git se pueden actualizar en el de Fossil de la siguiente manera:
git fast-export --import-marks=../repo/git.marks \ --export-marks=../repo/git.marks --all | fossil import --git \ --incremental --import-marks ../repo/fossil.marks \ --export-marks ../repo/fossil.marks ../repo.fossil cd ../repo fossil push
Como digo, no conseguí que me funcionara. Sólo lo he intentado una vez para un proyecto muy concreto, que se inició y lo tengo en un repositorio de Codeberg y que quería hacer el seguimiento también con Fossil. Tras una mañana de tirarme de los pelos decidí abandonar y dejar que continuara sólo en repositorio de Git.
Conclusiones
Llevo un año utilizando Fossil, me proporciona un montón de herramientas que utilizo en mis repositorios privados. No lo he utilizado, al menos aún, en uno público o compartido con alguien. Por tanto, mi experiencia es limitada a mis cosas. Y lo suelo utilizar siempre o casi siempre a través de línea de comandos, incluso para los settings. Aunque suelo consultar los repos, escribir en los wikis o crear los tickets desde la interfaz gráfica del navegador.
Aunque es un rival para git
, sigo utilizando este, por tanto, no lo
veo una alternativa. A mí me va bien para mis cosas, pero si
participo en otro proyecto con más gente lo habitual es que utilicen
git
, y no me molesta, no creo que sean excluyentes para mi modo de
trabajar. Sí es cierto que cuando inicio un proyecto nuevo lo hago con
Fossil. Además he estado siguiendo algunos vídeos de su creador
Richard Hipp, creador también de SQLite. En ellos critica git
,
bien, bueno... ¿qué va a decir el padre de la otra criatura?... pues
eso, mi niño es el mejor.
Había preparado un ejercicio más largo con más respuestas a más preguntas, pero creo que ya me ha quedado un ladrillo bastante infumable, así que os dejo una de las capturas:
Iba a ilustrar una de las preguntas que era si se pueden cambiar los
mensajes del commit
una vez en el repositorio. La respuesta es sí,
se puede, en la imagen se puede ver que se ha hecho y que
automáticamente ha generado otro evento en la timeline que dice
que la hemos cambiado (el de encima de la pila).
Me dejo en el tintero el tema de gestión de usuarios en repositorios remotos, pero como eso también lo tenéis en la documentación que viene con Fossil y realmente es bastante sencillo, no le doy más vueltas al tema. Además, la interfaz gráfica para dicha gestión está plagada de ayuda para aclararte lo que haces, sólo tienes que leer.
Footnotes:
Que por cierto, a los talibanes de la filosofía Unix que usan Emacs... en fin.
Comentarios