Custom formatting of command-line timeline
(1) By Daniel Dumitriu (danield) on 2020-12-09 19:45:53 [source]
Hi,
In some case it can be useful to be able to format the command-line timeline - whether or not this can be used for automation. Below is a patch against the current tip [037c79476c] that tries to make a step in this direction. Comments are welcomed.
Index: src/descendants.c
==================================================================
--- src/descendants.c
+++ src/descendants.c
@@ -382,11 +382,11 @@
"%s"
" AND event.objid IN (SELECT rid FROM leaves)"
" ORDER BY event.mtime DESC",
timeline_query_for_tty()
);
- print_timeline(&q, 0, width, 0);
+ print_timeline(&q, 0, width, 0, 0);
db_finalize(&q);
}
/*
** COMMAND: leaves*
Index: src/search.c
==================================================================
--- src/search.c
+++ src/search.c
@@ -643,11 +643,11 @@
blob_append_sql(&sql,"AND x>%d ", iBest/3);
}
blob_append(&sql, "ORDER BY x DESC, date DESC ", -1);
db_prepare(&q, "%s", blob_sql_text(&sql));
blob_reset(&sql);
- print_timeline(&q, nLimit, width, 0);
+ print_timeline(&q, nLimit, width, 0, 0);
db_finalize(&q);
}
#if INTERFACE
/* What to search for */
Index: src/tag.c
==================================================================
--- src/tag.c
+++ src/tag.c
@@ -541,11 +541,11 @@
" ORDER BY event.mtime DESC /*sort*/",
timeline_query_for_tty(), zType, tagid
);
db_prepare(&q, "%s", blob_sql_text(&sql));
blob_reset(&sql);
- print_timeline(&q, nFindLimit, 79, 0);
+ print_timeline(&q, nFindLimit, 79, 0, 0);
db_finalize(&q);
}
}
}else
Index: src/timeline.c
==================================================================
--- src/timeline.c
+++ src/timeline.c
@@ -2727,10 +2727,106 @@
@ ↓</a>
}
document_emit_js(/*handles pikchrs rendered above*/);
style_finish_page("timeline");
}
+
+/*
+** Translate a timeline entry into the printable format by
+** converting every %-substitutions as follows:
+**
+** %n newline
+** %% a raw %
+** %H commit hash
+** %h abbreviated commit hash
+** %a author name
+** %d date
+** %c comment (\n is replaced by space, \r is deleted)
+** %b branch
+** %t tags
+** %p prefix (one or more of: current, merge, unpublished,
+** fork, branch, leaf)
+**
+** The returned string is obtained from fossil_malloc() and should
+** be freed by the caller.
+*/
+static char *timeline_entry_subst(
+ const char *zFormat,
+ int *nLine,
+ const char *zId,
+ const char *zDate,
+ const char *zUser,
+ const char *zCom,
+ const char *zBranch,
+ const char *zTags,
+ const char *zPrefix
+){
+ Blob co;
+ int j;
+ blob_init(&co, 0, 0);
+ *nLine = 1;
+ /* Replace LF and tab with space, delete CR */
+ while( zCom[0] ){
+ for(j=0; zCom[j] && zCom[j]!='\r' && zCom[j]!='\n' && zCom[j]!='\t'; j++){}
+ blob_append(&co, zCom, j);
+ if( zCom[j]==0 ) break;
+ if( zCom[j]!='\r')
+ blob_append(&co, " ", 1);
+ zCom += j+1;
+ }
+ blob_str(&co);
+
+ Blob r;
+ int i;
+ blob_init(&r, 0, 0);
+ while( zFormat[0] ){
+ for(i=0; zFormat[i] && zFormat[i]!='%'; i++){}
+ blob_append(&r, zFormat, i);
+ if( zFormat[i]==0 ) break;
+ if( zFormat[i+1]=='%' ){
+ blob_append(&r, "%", 1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='n' ){
+ blob_append(&r, "\n", 1);
+ *nLine += 1;
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='H' ){
+ blob_append(&r, zId, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='h' ){
+ char *zFree = 0;
+ zFree = mprintf("%S", zId);
+ blob_append(&r, zFree, -1);
+ fossil_free(zFree);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='d' ){
+ blob_append(&r, zDate, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='a' ){
+ blob_append(&r, zUser, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='c' ){
+ blob_append(&r, co.aData, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='b' ){
+ if( zBranch ) blob_append(&r, zBranch, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='t' ){
+ blob_append(&r, zTags, -1);
+ zFormat += i+2;
+ }else if( zFormat[i+1]=='p' ){
+ blob_append(&r, zPrefix, -1);
+ zFormat += i+2;
+ }else{
+ blob_append(&r, zFormat+i, 1);
+ zFormat += i+1;
+ }
+ }
+ fossil_free(co.aData);
+ blob_str(&r);
+ return r.aData;
+}
/*
** The input query q selects various records. Print a human-readable
** summary of those records.
**
@@ -2745,18 +2841,21 @@
** The query should return these columns:
**
** 0. rid
** 1. uuid
** 2. Date/Time
-** 3. Comment string and user
+** 3. Comment string, user, and tags
** 4. Number of non-merge children
** 5. Number of parents
** 6. mtime
** 7. branch
** 8. event-type: 'ci', 'w', 't', 'f', and so forth.
+** 9. comment
+** 10. user
+** 11. tags
*/
-void print_timeline(Stmt *q, int nLimit, int width, int verboseFlag){
+void print_timeline(Stmt *q, int nLimit, int width, const char *zFormat, int verboseFlag){
int nAbsLimit = (nLimit >= 0) ? nLimit : -nLimit;
int nLine = 0;
int nEntry = 0;
char zPrevDate[20];
const char *zCurrentUuid = 0;
@@ -2775,11 +2874,15 @@
const char *zId = db_column_text(q, 1);
const char *zDate = db_column_text(q, 2);
const char *zCom = db_column_text(q, 3);
int nChild = db_column_int(q, 4);
int nParent = db_column_int(q, 5);
+ const char *zBranch = db_column_text(q, 7);
const char *zType = db_column_text(q, 8);
+ const char *zComShort = db_column_text(q, 9);
+ const char *zUserShort = db_column_text(q, 10);
+ const char *zTags = db_column_text(q, 11);
char *zFree = 0;
int n = 0;
char zPrefix[80];
if( nAbsLimit!=0 ){
@@ -2789,17 +2892,18 @@
}else if( nEntry>=nAbsLimit ){
fossil_print("--- entry limit (%d) reached ---\n", nAbsLimit);
break; /* entry count limit hit, stop. */
}
}
- if( fossil_strnicmp(zDate, zPrevDate, 10) ){
+ if( zFormat == 0 && fossil_strnicmp(zDate, zPrevDate, 10) ){
fossil_print("=== %.10s ===\n", zDate);
memcpy(zPrevDate, zDate, 10);
nLine++; /* record another line */
}
if( zCom==0 ) zCom = "";
- fossil_print("%.8s ", &zDate[11]);
+ if( zFormat == 0 )
+ fossil_print("%.8s ", &zDate[11]);
zPrefix[0] = 0;
if( nParent>1 ){
sqlite3_snprintf(sizeof(zPrefix), zPrefix, "*MERGE* ");
n = strlen(zPrefix);
}
@@ -2831,12 +2935,27 @@
zFree = mprintf("[%S] Edit to wiki page \"%s\"", zId, zCom+1);
}
}else{
zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom);
}
- /* record another X lines */
- nLine += comment_print(zFree, zCom, 9, width, get_comment_format());
+
+ if( zFormat ){
+ if( nChild==0 ){
+ sqlite3_snprintf(sizeof(zPrefix)-n, &zPrefix[n], "*LEAF* ");
+ }
+ char *zEntry;
+ int nEntryLine = 0;
+ zEntry = timeline_entry_subst(zFormat, &nEntryLine, zId, zDate, zUserShort,
+ zComShort, zBranch, zTags, zPrefix);
+ nLine += nEntryLine;
+ fossil_print("%s\n", zEntry);
+ fossil_free(zEntry);
+ }
+ else{
+ /* record another X lines */
+ nLine += comment_print(zFree, zCom, 9, width, get_comment_format());
+ }
fossil_free(zFree);
if(verboseFlag){
if( !fchngQueryInit ){
db_prepare(&fchngQuery,
@@ -2865,10 +2984,13 @@
}
nLine++; /* record another line */
}
db_reset(&fchngQuery);
}
+ /* Except for "oneline", separate formatted entries by one empty line */
+ if( zFormat && fossil_strcmp(zFormat, "%h %c")!=0 )
+ fossil_print("\n");
nEntry++; /* record another complete entry */
}
if( rc==SQLITE_DONE ){
/* Did the underlying query actually have all entries? */
if( nAbsLimit==0 ){
@@ -2902,10 +3024,17 @@
@ AS primPlinkCount,
@ (SELECT count(*) FROM plink WHERE cid=blob.rid) AS plinkCount,
@ event.mtime AS mtime,
@ tagxref.value AS branch,
@ event.type
+ @ , coalesce(ecomment,comment) AS comment0
+ @ , coalesce(euser,user,'?') AS user0
+ @ , (SELECT case when length(x)>0 then x else '' end
+ @ FROM (SELECT group_concat(substr(tagname,5), ', ') AS x
+ @ FROM tag, tagxref
+ @ WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid
+ @ AND tagxref.rid=blob.rid AND tagxref.tagtype>0)) AS tags
@ FROM tag CROSS JOIN event CROSS JOIN blob
@ LEFT JOIN tagxref ON tagxref.tagid=tag.tagid
@ AND tagxref.tagtype>0
@ AND tagxref.rid=blob.rid
@ WHERE blob.rid=event.objid
@@ -2931,10 +3060,11 @@
*/
static int fossil_is_julianday(const char *zDate){
return db_int(0, "SELECT EXISTS (SELECT julianday(%Q) AS jd"
" WHERE jd IS NOT NULL)", zDate);
}
+
/*
** COMMAND: timeline
**
** Usage: %fossil timeline ?WHEN? ?CHECKIN|DATETIME? ?OPTIONS?
@@ -2977,10 +3107,27 @@
** etc.) after the check-in comment.
** -W|--width N Width of lines (default is to auto-detect). N must be
** either greater than 20 or it ust be zero 0 to
** indicate no limit, resulting in a single line per
** entry.
+** -F|--format Entry format. Values "oneline", "medium", and "full"
+** get mapped to the full options below.
+** Otherwise a string which can contain these placeholders:
+** %n newline
+** %% a raw %
+** %H commit hash
+** %h abbreviated commit hash
+** %a author name
+** %d date
+** %c comment (\\n,\\t replaced by space, \\r deleted)
+** %b branch
+** %t tags
+** %p zero or more of: current, merge, unpublished,
+** fork, branch, leaf
+** --oneline Show only short hash and comment for each entry
+** --medium Medium-verbose entry formatting
+** --full Extra verbose entry formatting
** -R REPO_FILE Specifies the repository db to use. Default is
** the current checkout's repository.
*/
void timeline_cmd(void){
Stmt q;
@@ -2996,10 +3143,11 @@
Blob uuid;
int mode = TIMELINE_MODE_NONE;
int verboseFlag = 0 ;
int iOffset;
const char *zFilePattern = 0;
+ const char *zFormat = 0;
Blob treeName;
int showSql = 0;
verboseFlag = find_option("verbose","v", 0)!=0;
if( !verboseFlag){
@@ -3008,10 +3156,17 @@
db_find_and_open_repository(0, 0);
zLimit = find_option("limit","n",1);
zWidth = find_option("width","W",1);
zType = find_option("type","t",1);
zFilePattern = find_option("path","p",1);
+ zFormat = find_option("format","F",1);
+ if( find_option("oneline",0,0)!= 0 || fossil_strcmp(zFormat,"oneline")==0 )
+ zFormat = "%h %c";
+ if( find_option("medium",0,0)!= 0 || fossil_strcmp(zFormat,"medium")==0 )
+ zFormat = "Commit: %h%nDate: %d%nAuthor: %a%nComment: %c";
+ if( find_option("full",0,0)!= 0 || fossil_strcmp(zFormat,"full")==0 )
+ zFormat = "Commit: %H%nDate: %d%nAuthor: %a%nComment: %c%nBranch: %b%nTags: %t%nPrefix: %p";
showSql = find_option("sql",0,0)!=0;
if( !zLimit ){
zLimit = find_option("count",0,1);
}
@@ -3157,11 +3312,11 @@
if( showSql ){
fossil_print("%s\n", blob_str(&sql));
}
db_prepare_blob(&q, &sql);
blob_reset(&sql);
- print_timeline(&q, n, width, verboseFlag);
+ print_timeline(&q, n, width, zFormat, verboseFlag);
db_finalize(&q);
}
/*
** WEBPAGE: thisdayinhistory
Index: src/update.c
==================================================================
--- src/update.c
+++ src/update.c
@@ -221,11 +221,11 @@
"%s "
" AND event.objid IN leaves"
" ORDER BY event.mtime DESC",
timeline_query_for_tty()
);
- print_timeline(&q, -100, width, 0);
+ print_timeline(&q, -100, width, 0, 0);
db_finalize(&q);
fossil_fatal("Multiple descendants");
}
}
tid = db_int(0, "SELECT rid FROM leaves, event"
(2) By Daniel Dumitriu (danield) on 2020-12-18 19:27:16 in reply to 1 [link] [source]
I have committed the changes to a separate branch for inspection.
(3) By Stephan Beal (stephan) on 2020-12-19 08:24:14 in reply to 2 [link] [source]
I have committed the changes to a separate branch for inspection.
Good morning!
%p
==> prefix
, but prefix implies "at the beginning," which is contrary to the point of making it configurable: in "%H %a %p"
it isn't a prefix. i recommend renaming it to %s
for "status" or "state", or some such.
blob_str(&co);
Blob r;
int i;
blob_init(&r, 0, 0);
...
Declaring vars anywhere but the top of a scope is a C++'ism/C99-ism. This project's style calls for placing all such declarations at the top of their scope.
Thinking of potential future uses: rather than return a string, how about passing in the blob as the first argument, and appending to it (rather than resetting it)? Internally you already use the blob, so it would be a simply matter of moving that into the parameters. On the other hand, it's already got more than a handful of parameters. That many parameters usually begs for transforming it into a struct to hold them, but that might be overkill for this case. Something along the lines of:
struct TimelineFields_{
const char *zId;
const char *zDate;
const char *zUser;
const char *zCom;
const char *zBranch;
const char *zTags;
const char *zPrefix;
};
That's not a suggestion/recommendation, just food for thought.
(4) By Daniel Dumitriu (danield) on 2020-12-19 16:05:08 in reply to 3 [link] [source]
Declaring vars anywhere but the top of a scope is a C++'ism/C99-ism. This project's style calls for placing all such declarations at the top of their scope.
Right, this just slipped me.
%p ==> prefix, but prefix implies "at the beginning," which is contrary to the point of making it configurable: in "%H %a %p" it isn't a prefix. i recommend renaming it to %s for "status" or "state", or some such.
That's right, I've taken this over from the existing timeline, where it is a prefix. Now it's up to the native speakers to pick up a word - actually "tags" would fit, but it's already used - such as mark(er)s, notes?
Thinking of potential future uses: rather than return a string, how about passing in the blob as the first argument, and appending to it (rather than resetting it)? Internally you already use the blob, so it would be a simply matter of moving that into the parameters.
I am confortable with returning a string, but maybe I just don't see how appending to the blob would help with future uses. Do you have a clear preference here, Richard?
On the other hand, it's already got more than a handful of parameters. That many parameters usually begs for transforming it into a struct to hold them, but that might be overkill for this case.
I had also thought about this, but right for your reason I decided to keep them all as parameters.
(5) By Stephan Beal (stephan) on 2020-12-19 16:18:52 in reply to 4 [link] [source]
Now it's up to the native speakers to pick up a word - actually "tags" would fit, but it's already used - such as mark(er)s, notes?
My only current suggestion is "status" or "state", both of which would presumably use %s
, but perhaps someone else has a better-fitting suggestion for how that field should be called.
I am confortable with returning a string, but maybe I just don't see how appending to the blob would help with future uses.
Admittedly, i have a tendency towards designing APIs in the grand tradition of "YAGNI" (You Ain't Gonna Need It). If a real use case does come up, it would be simple to refactor it, so leaving it as is can't hurt. (Rather than take a blob, i'd actually like to see it take a callback and a void state pointer for that callback, but that's far, far more generic than fossil's internals need!)
(6) By John Rouillard (rouilj) on 2020-12-19 17:09:20 in reply to 5 [link] [source]
Under the rubric of 'naming things is hard', would phase
work (with %p still
as the token)?
(7) By Stephan Beal (stephan) on 2021-01-13 12:43:28 in reply to 1 [link] [source]
Custom formatting of command-line timeline
Today i finally tried this out and am curious whether...
$ f time --format "%h:%b" -type ci | head
f2ec37e45e:trunk
f953a1638b:trunk
3cd0b26414:trunk
7bf929984a:trunk
eec971fdeb:trunk
... the extra blank lines are a feature or a bug?
(8) By Daniel Dumitriu (danield) on 2021-01-13 12:54:18 in reply to 7 [link] [source]
Probably bug. I'll look soon into it.
(9) By Daniel Dumitriu (danield) on 2021-01-13 13:37:59 in reply to 7 [link] [source]
The bug is fixed now.
I had introduced a fork in the process, since my autosync
had been somehow still turned off, sorry - closed now.
(10) By Andy Bradford (andybradford) on 2021-01-13 15:25:45 in reply to 9 [link] [source]
> I had introduced a fork in the process, ... sorry - closed now. I see no reason to apologize for a fork. Forking is a natural process in Fossil. Andy