Fossil Forum

Custom formatting of command-line timeline
Login

Custom formatting of command-line timeline

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 @@
     @ &nbsp;&darr;</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