Repository: iterative/dvc

Issue:
Feature request: show which dependencies changed when calling dvc repro
I have a situation where dvc repro reproduces the original command while I don't expect it to. (Further investigation as to why this happens is ongoing).

It would be interesting to have an option in the dvc repro command to print out which dependencies changed when dvc repro is called.


Discussion:
Hi @tdeboissiere !

We have a `dvc status` command which shows which dependencies changed. Maybe it will help with the investigation. That being said, I agree that it would be handy if `dvc repro` actually said that such and such dependencies changed before reproducing. I will look into implementing it shortly.

Thanks,
Ruslan
Thanks for the quick pointer !
Looking forward to it.

Resolving patch:
diff --git a/dvc/logger.py b/dvc/logger.py
--- a/dvc/logger.py
+++ b/dvc/logger.py
@@ -19,9 +19,16 @@ class Logger(object):
     }
 
     COLOR_MAP = {
-        'debug': colorama.Fore.BLUE,
-        'warn': colorama.Fore.YELLOW,
-        'error': colorama.Fore.RED
+        'green': colorama.Fore.GREEN,
+        'yellow': colorama.Fore.YELLOW,
+        'blue': colorama.Fore.BLUE,
+        'red': colorama.Fore.RED,
+    }
+
+    LEVEL_COLOR_MAP = {
+        'debug': 'blue',
+        'warn': 'yellow',
+        'error': 'red',
     }
 
     def __init__(self, loglevel=None):
@@ -58,12 +65,12 @@ def be_verbose():
         Logger.logger().setLevel(logging.DEBUG)
 
     @staticmethod
-    def colorize(msg, typ):
+    def colorize(msg, color):
         header = ''
         footer = ''
 
         if sys.stdout.isatty():  # pragma: no cover
-            header = Logger.COLOR_MAP.get(typ.lower(), '')
+            header = Logger.COLOR_MAP.get(color.lower(), '')
             footer = colorama.Style.RESET_ALL
 
         return u'{}{}{}'.format(header, msg, footer)
@@ -84,7 +91,8 @@ def parse_exc(exc, tb=None):
 
     @staticmethod
     def _prefix(msg, typ):
-        return Logger.colorize('{}: '.format(msg), typ)
+        color = Logger.LEVEL_COLOR_MAP.get(typ.lower(), '')
+        return Logger.colorize('{}: '.format(msg), color)
 
     @staticmethod
     def error_prefix():
diff --git a/dvc/stage.py b/dvc/stage.py
--- a/dvc/stage.py
+++ b/dvc/stage.py
@@ -1,6 +1,5 @@
 import os
 import yaml
-import itertools
 import posixpath
 import subprocess
 from schema import Schema, SchemaError, Optional, Or, And
@@ -135,8 +134,6 @@ def changed_md5(self):
         if self.md5 and md5 and self.md5 == md5:
             return False
 
-        msg = "Dvc file '{}' md5 changed(expected '{}', actual '{}')"
-        self.project.logger.debug(msg.format(self.relpath, self.md5, md5))
         return True
 
     @property
@@ -149,30 +146,51 @@ def is_import(self):
                len(self.deps) == 1 and \
                len(self.outs) == 1
 
-    def changed(self):
+    def changed(self, print_info=False):
         ret = False
 
+        if print_info:
+            log = self.project.logger.info
+        else:
+            log = self.project.logger.debug
+
         if self.is_callback:
+            msg = "Dvc file '{}' is a 'callback' stage (has a command and " \
+                  "no dependencies) and thus always considered as changed."
+            self.project.logger.warn(msg.format(self.relpath))
             ret = True
 
-        if self.locked:
-            entries = self.outs
-        else:
-            entries = itertools.chain(self.outs, self.deps)
-
-        for entry in entries:
-            if entry.changed():
+        if not self.locked:
+            for dep in self.deps:
+                if not dep.changed():
+                    continue
+                if print_info:
+                    msg = "Dependency '{}' of '{}' changed."
+                    log(msg.format(dep, self.relpath))
                 ret = True
 
+        for out in self.outs:
+            if not out.changed():
+                continue
+            if print_info:
+                msg = "Output '{}' of '{}' changed."
+                log(msg.format(out, self.relpath))
+            ret = True
+
         if self.changed_md5():
+            if print_info:
+                msg = "Dvc file '{}' changed."
+                log(msg.format(self.relpath))
             ret = True
 
         if ret:
-            msg = u'Dvc file \'{}\' changed'.format(self.relpath)
+            msg = "Stage '{}' changed.".format(self.relpath)
+            color = 'yellow'
         else:
-            msg = u'Dvc file \'{}\' didn\'t change'.format(self.relpath)
+            msg = "Stage '{}' didn't change.".format(self.relpath)
+            color = 'green'
 
-        self.project.logger.debug(msg)
+        log(Logger.colorize(msg, color))
 
         return ret
 
@@ -185,7 +203,7 @@ def remove(self):
         os.unlink(self.path)
 
     def reproduce(self, force=False, dry=False, interactive=False):
-        if not self.changed() and not force:
+        if not self.changed(print_info=True) and not force:
             return None
 
         if (self.cmd or self.is_import) and not self.locked and not dry:


Test patch: