Moving from CVS to Git: $Id:$ equivalent?

2020-01-24 19:00发布

问题:

I read through a bunch of questions asking about simple source code control tools and Git seemed like a reasonable choice. I have it up and running, and it works well so far. One aspect that I like about CVS is the automatic incrementation of a version number.

I understand that this makes less sense in a distributed repository, but as a developer I want/need something like this. Let me explain why:

I use Emacs. Periodically I go through and look for new versions of the Lisp source files for third-party packages. Say I've got a file, foo.el, which, according to the header, is version 1.3; if I look up the latest version and see it's 1.143 or 2.6 or whatever, I know I'm pretty far behind.

If instead I see a couple of 40-character hashes, I won't know which is later or get any idea of how much later it is. I would absolutely hate it if I had to manually check ChangeLogs just to get an idea of how out of date I am.

As a developer, I want to extend this courtesy, as I see it, to the people that use my output (and maybe I'm kidding myself that anyone is, but let's leave that aside for a moment). I don't want to have to remember to increment the damn number myself every time, or a timestamp or something like that. That's a real PITA, and I know that from experience.

So what alternatives do I have? If I can't get an $Id:$ equivalent, how else can I provide what I'm looking for?

I should mention that my expectation is that the end user will NOT have Git installed and even if they do, will not have a local repository (indeed, I expect not to make it available that way).

回答1:

The SHA is just one representation of a version (albeit canonical). The git describe command offers others and does so quite well.

For example, when I run git describe in my master branch of my Java memcached client source, I get this:

2.2-16-gc0cd61a

That says two important things:

  1. There have been exactly 16 commits in this tree since 2.2
  2. The exact source tree can be displayed on anyone else's clone.

Let's say, for example, you packaged a version file with the source (or even rewrote all the content for distribution) to show that number. Let's say that packaged version was 2.2-12-g6c4ae7a (not a release, but a valid version).

You can now see exactly how far behind you are (4 commits), and you can see exactly which 4 commits:

# The RHS of the .. can be origin/master or empty, or whatever you want.
% git log --pretty=format:"%h %an %s" 2.2-12-g6c4ae7a..2.2-16-gc0cd61a
c0cd61a Dustin Sallings More tries to get a timeout.
8c489ff Dustin Sallings Made the timeout test run on every protocol on every bui
fb326d5 Dustin Sallings Added a test for bug 35.
fba04e9 Valeri Felberg Support passing an expiration date into CAS operations.


回答2:

By now there is support for $Id:$ in Git. To enable it for file README you would put "README ident" into .gitattributes. Wildcards on file names are supported. See man gitattributes for details.



回答3:

This isn't an unreasonable request from the OP.

My use-case is:

  1. I use Git for my own personal code, therefore no collaboration with others.
  2. I keep system Bash scripts in there which might go into /usr/local/bin when they are ready.

I use three separate machines with the same Git repository on it. It would be nice to know what "version" of the file I have currently in /usr/local/bin without having to do a manual "diff -u <repo version> <version in /usr/local/bin>".

To those of you being negative, remember there are other use cases out there. Not everyone uses Git for collaborative work with the files in the Git repository being their "final" location.

Anyway, the way I did it was to create an attributes file in the repository like this:

cat .git/info/attributes
# see man gitattributes
*.sh ident
*.pl ident
*.cgi ident

Then put $Id$ somewhere in the file (I like to put it after the shebang).

The commit. Note that this doesn't automatically do the expansion like I expected. You have to re-co the file, for example,

git commit foo.sh
rm foo.sh
git co foo.sh

And then you will see the expansion, for example:

$ head foo.sh
#!/bin/sh

# $Id: e184834e6757aac77fd0f71344934b1cd774e6d4 $

Some good information is in How do I enable the ident string for a Git repository?.



回答4:

Not sure this will ever be in Git. To quote Linus:

"The whole notion of keyword substitution is just totally idiotic. It's trivial to do "outside" of the actual content tracking, if you want to have it when doing release trees as tar-balls etc."

It's pretty easy to check the log, though - if you're tracking foo.el's stable branch, you can see what new commits are in the stable branch's log that aren't in your local copy. If you want to simulate CVS's internal version number, you can compare the timestamp of the last commit.

Edit: you should write or use someone else's scripts for this, of course, not do this manually.



回答5:

As I’ve written before:

Having automatically generated Id tags that show a sensible version number is impossible to do with DSCM tools like Bazaar because everybody’s line of development can be different from all others. So somebody could refer to version “1.41” of a file but your version “1.41” of that file is different.

Basically, $Id$ does not make any sense with Bazaar, Git, and other distributed source code management tools.



回答6:

I had the same problem. I needed to have a version that was simpler than a hash string and available for people using the tool without needing to connect to the repository.

I did it with a Git pre-commit hook and changed my script to be able to automatically update itself.

I base the version off of the number of commits done. This is a slight race condition because two people could commit at the same time and both think they are committing the same version number, but we don't have many developers on this project.

Mine is in Ruby, but it's not terribly complex code. The Ruby script has:

MYVERSION = '1.090'
## Call script to do updateVersion from .git/hooks/pre-commit
def updateVersion
  # We add 1 because the next commit is probably one more - though this is a race
  commits = %x[git log #{$0} | grep '^commit ' | wc -l].to_i + 1
  vers = "1.%0.3d" % commits

  t = File.read($0)
  t.gsub!(/^MYVERSION = '(.*)'$/, "MYVERSION = '#{vers}'")
  bak = $0+'.bak'
  File.open(bak,'w') { |f| f.puts t }
  perm = File.stat($0).mode & 0xfff
  File.rename(bak,$0)
  File.chmod(perm,$0)
  exit
end

And then I have a command-line option (-updateVersion) that calls updateVersion for the tool.

Finally, I go to the Git head and create an executable script in .git/hooks/pre-commit.

The script simply changes to the head of the Git directory and calls my script with -updateVersion.

Every time I check in, the MYVERSION variable is updated based on what the number of commits will be.



回答7:

If having $Keywords$ is essential for you, then maybe you could try to look at Mercurial instead? It has a hgkeyword extension that implement what you want. Mercurial is interesting as a DVCS anyway.



回答8:

Something that is done with Git repositories is to use the tag object. This can be used to tag a commit with any kind of string and can be used to mark versions. You can see that tags in a repository with the git tag command, which returns all the tags.

It's easy to check out a tag. For example, if there is a tag v1.1 you can check that tag out to a branch like this:

git checkout -b v1.1

As it's a top level object, you'll see the whole history to that commit, as well as be able to run diffs, make changes, and merges.

Not only that, but a tag persists, even if the branch that it was on has been deleted without being merged back into the main line.



回答9:

If you're just wanting people to be able to get an idea how far out of date they are, Git can inform them of that in several fairly easy ways. They compare the dates of the last commit on their trunk and your trunk, for example. They can use git cherry to see how many commits have occurred in your trunk that are not present in theirs.

If that's all you want this for, I'd look for a way to provide it without a version number.

Also, I wouldn't bother extending the courtesy to anyone unless you're sure they want it. :)



回答10:

To apply the expansion to all files in all sub-directories in the repository, add a .gitattributes file to the top level directory in the repository (i.e. where you'd normally put the .gitignore file) containing:

* ident

To see this in effect, you'll need to do an effective checkout of the file(s) first, such as deleting or editing them in any way. Then restore them with:

git checkout .

And you should see $Id$ replaced with something like:

$Id: ea701b0bb744c90c620f315e2438bc6b764cdb87 $

From man gitattributes:

ident

When the attribute ident is set for a path, Git replaces $Id$ in the blob object with $Id:, followed by the 40-character hexadecimal blob object name, followed by a dollar sign $ upon checkout. Any byte sequence that begins with $Id: and ends with $ in the worktree file is replaced with $Id$ upon check-in.

This ID will change every time a new version of the file is committed.



回答11:

If I understand correctly, essentially, you want to know how many commits have happened on a given file since you last updated.

First get the changes in the remote origin, but don't merge them into your master branch:

% git fetch

Then get a log of the changes that have happened on a given file between your master branch and the remote origin/master.

% git log master..origin/master foo.el

This gives you the log messages of all the commits that have happened in the remote repository since you last merged origin/master into your master.

If you just want a count of the changes, pipe it to wc. Say, like this:

% git rev-list master..origin/master foo.el | wc -l


回答12:

RCS IDs are nice for single-file projects, but for any other the $Id$ says nothing about the project (unless you do forced dummy check-ins to a dummy version file).

Still one might be interested how to get the equivalents of $Author$, $Date$, $Revision$, $RCSfile$, etc. on a per file level or at the commit level (how to put them where some keywords are is another question). I don't have an answer on these, but see the requirement to update those, especially when the files (now in Git) originated from RCS-compatible systems (CVS).

Such keywords may be interesting if the sources are distributed separately from any Git repository (that's what I also do). My solution is like this:

Every project has a directory of its own, and in the project root I have a text file named .version which content describes the current version (the name that will be used when exporting the sources).

While working for the next release a script extracts that .version number, some Git version descriptor (like git describe) and a monotonic build number in .build (plus host and date) to an auto-generated source file that is linked to the final program, so you can find out from what source and when it was built.

I develop new features in separate branches, and the first thing I do is add n (for "next") to the .version string (multiple branches originating from the same root would use the same temporary .version number). Before release I decide which branches to merge (hopefully all having the same .version). Before committing the merge, I update .version to the next number (major or minor update, depending on the merged features).



回答13:

I agree with those who think that token replacement belongs to build tools rather than to version control tools.

You should have some automated release tool to set the version IDs in your sources at the time the release is being tagged.



回答14:

If you want the git commit information accessible into your code, then you have to do a pre-build step to get it there. In bash for C/C++ it might look something like this:

prebuild.sh

#!/bin/bash
commit=$(git rev-parse HEAD)
tag=$(git describe --tags --always ${commit})
cat <<EOF >version.c
#include "version.h"
const char* git_tag="${tag}";
const char* git_commit="${commit}";
EOF

with version.h looking like:

#pragma once
const char* git_tag;
const char* git_commit;

Then, wherever you need it in your code #include "version.h" and reference git_tag or git_commit as needed.

And your Makefile might have something like this:

all: package
version:
  ./prebuild.sh
package: version
  # the normal build stuff for your project

This has the benefit of:

  • getting the currently correct values for this build regardless of branching, merging cherry-picking and such.

This implementation of prepublish.sh has the drawbacks of:

  • forcing a recompile even if the git_tag/git_commit didn't change.
  • it does not take into account local modified files that have not been committed but effect the build.
    • use git describe --tags --always --dirty to catch that use-case.
  • pollutes the global namespace.

A fancier prebuild.sh that could avoid these issues is left as an exercise for the reader.



回答15:

Since you use Emacs, you might be lucky :)

I've came across this question by coincidence, and also by coincidence I've came by Lively few days ago, an Emacs package which allows having lively pieces of Emacs Lisp in your document. I've not tried it to be honest, but it came to my mind when reading this.



回答16:

I also came from SCCS, RCS, and CVS (%W% %G% %U%).

I had a similar challenge. I wanted to know what version a piece of code was on any system running it. The system may or may not be connected to any network. The system may or may not have Git installed. The system may or may not have the GitHub repository installed on it.

I wanted the same solution for several types of code (.sh, .go, .yml, .xml, etc). I wanted any person without knowledge of Git or GitHub to be able to answer the question "What version are you running?"

So, I wrote what I call a wrapper around a few Git commands. I use it to mark a file with a version number and some information. It solves my challenge. It may help you.

https://github.com/BradleyA/markit

git clone https://github.com/BradleyA/markit
cd markit


回答17:

To resolve this issue for myself, I created small "hack" as post-commit hook:

echo | tee --append *
git checkout *

In more detail documented in this post on my blog.