When refreshing patches, the default action is to include every path
which is changed relative to the current working directory. This common
workflow creates problems when one uses a submodule. Due to the nature
of submodules, under some workflows they can often be out-of-sync, such
as when working on new updates within a submodule.
Accidental refreshes which include the submodule changes often occur,
which leads to commits which unintentionally include changes to
submodule pointers. These commits likely cause submodules to point to
incorrect locations.
Since it is not easy to convince many developers to always use "refresh
--index", instead, modify the default refresh behavior.
Instead of including all files, now check to make sure that we exclude
submodule files.
We can do this by using git ls-tree to list the files and their modes.
To avoid re-running ls-tree many times, we'll use a compiled regex to
match lines that look like submodules, and extract their filename.
Add a new option to refresh "--submodules" which tells refresh to
include submodules regardless. Additionally, add a "--no-submodules" and
a stg.refresh.submodules configuration so that users may opt to configure
this behavior.
Since the number of users who use submodules is likely small, make this
the new default behavior.
stgit/commands/refresh.py | 38 +++++++++++++++++++++++++++++---
stgit/config.py | 31 ++++++++++++++-------------
stgit/lib/git.py | 9 ++++++++
t/t5000-refresh-submodules.sh | 50 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 110 insertions(+), 18 deletions(-)
create mode 100755 t/t5000-refresh-submodules.sh
diff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py
index d17ccc2cf98b..223adadcf68a 100644
--- a/stgit/commands/refresh.py
+++ b/stgit/commands/refresh.py
@@ -45,6 +45,11 @@ You may optionally list one or more files or directories relative to
the current working directory; if you do, only matching files will be
updated.
+By default, stg refresh will ignore submodules when generating changes. This
+can be bypassed by passing --submodules, setting stg.refresh.submodules to
+yes, or adding a submodule change to the index and using --index to perform an
+index-based refresh.
+
Behind the scenes, stg refresh first creates a new temporary patch
with your updates, and then merges that patch into the patch you asked
to have refreshed. If you asked to refresh a patch other than the
@@ -79,7 +84,11 @@ options = [
opt('-e', '--edit', action = 'store_true',
short = 'Invoke an editor for the patch description'),
opt('-a', '--annotate', metavar = 'NOTE',
- short = 'Annotate the patch log entry')
+ short = 'Annotate the patch log entry'),
+ opt('-s', '--submodules', action = 'store_true',
+ short = 'Include submodules when refreshing patch contents'),
+ opt('', '--no-submodules', action = 'store_false', dest = 'submodules',
+ short = 'Exclude submodules when refreshing patch contents')
] + (argparse.message_options(save_template = False) +
argparse.hook_options() +
argparse.sign_options() + argparse.author_options())
@@ -99,7 +108,7 @@ def get_patch(stack, given_patch):
'Cannot refresh top patch, because no patches are applied')
return stack.patchorder.applied[-1]
-def list_files(stack, patch_name, args, index, update):
+def list_files(stack, patch_name, args, index, update, submodules):
"""Figure out which files to update."""
if index:
# --index: Don't update the index.
@@ -110,6 +119,15 @@ def list_files(stack, patch_name, args, index, update):
# --update: Restrict update to the paths that were already
# part of the patch.
paths &= stack.patches.get(patch_name).files()
+ else:
+ # Avoid including submodule files by default. This is to ensure that
+ # users in repositories with submodueles do not accidentally include
+ # submodule changes to patches just because they happen to have not
+ # run "git submodule update" prior to running stg refresh. We won't
+ # exclude them if we're explicitly told to include them, or if we're
+ # given explicit paths.
+ if not args and not submodules:
+ paths -= stack.repository.submodules(stack.head.data.tree)
return paths
def write_tree(stack, paths, temp_index):
@@ -253,6 +271,14 @@ def func(parser, options, args):
raise common.CmdException(
'You cannot --force a full refresh when using --index mode')
+ if options.update and options.submodules:
+ raise common.CmdException(
+ '--submodules is meaningless when only updating modified files')
+
+ if options.index and options.submodules:
+ raise common.CmdException(
+ '--submodules is meaningless when keeping the current index')
+
# If the user has not explicitly set the --index or --no-index parameters,
# we should lookup the default value from configuration. However, we do
# not want configuration to override any options on the command line.
@@ -262,9 +288,15 @@ def func(parser, options, args):
if options.index is None and not path_limiting and not options.force:
options.index = config.getbool('stgit.refresh.index')
+ # If submodules was not specified on the command line, infer a default
+ # from configuration. Similarly to index, do not infer a default if other
+ # command line options provided would already infer a false value.
+ if options.submodules is None and not options.index and not options.update:
+ options.submodules = config.getbool('stgit.refresh.submodules')
+
stack = directory.repository.current_stack
patch_name = get_patch(stack, options.patch)
- paths = list_files(stack, patch_name, args, options.index, options.update)
+ paths = list_files(stack, patch_name, args, options.index, options.update, options.submodules)
# Make sure there are no conflicts in the files we want to
# refresh.
diff --git a/stgit/config.py b/stgit/config.py
index 5a88548abc51..01d079b88b2a 100644
--- a/stgit/config.py
+++ b/stgit/config.py
@@ -32,21 +32,22 @@ class GitConfigException(StgException):
class GitConfig(object):
__defaults = {
- 'stgit.smtpserver': ['localhost:25'],
- 'stgit.smtpdelay': ['5'],
- 'stgit.pullcmd': ['git pull'],
- 'stgit.fetchcmd': ['git fetch'],
- 'stgit.pull-policy': ['pull'],
- 'stgit.autoimerge': ['no'],
- 'stgit.keepoptimized': ['no'],
- 'stgit.refresh.index': ['no'],
- 'stgit.shortnr': ['5'],
- 'stgit.pager': ['less'],
- 'stgit.alias.add': ['git add'],
- 'stgit.alias.rm': ['git rm'],
- '
stgit.alias.mv': ['git mv'],
- 'stgit.alias.resolved': ['git add'],
- 'stgit.alias.status': ['git status -s']
+ 'stgit.smtpserver': ['localhost:25'],
+ 'stgit.smtpdelay': ['5'],
+ 'stgit.pullcmd': ['git pull'],
+ 'stgit.fetchcmd': ['git fetch'],
+ 'stgit.pull-policy': ['pull'],
+ 'stgit.autoimerge': ['no'],
+ 'stgit.keepoptimized': ['no'],
+ 'stgit.refresh.index': ['no'],
+ 'stgit.refresh.submodules': ['no'],
+ 'stgit.shortnr': ['5'],
+ 'stgit.pager': ['less'],
+ 'stgit.alias.add': ['git add'],
+ 'stgit.alias.rm': ['git rm'],
+ '
stgit.alias.mv': ['git mv'],
+ 'stgit.alias.resolved': ['git add'],
+ 'stgit.alias.status': ['git status -s']
}
__cache = None
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index 82b7a900afb7..62b3377c7cc5 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -815,6 +815,15 @@ class Repository(RunWithEnv):
return None
finally:
index.delete()
+ def submodules(self, tree):
+ """Given a L{Tree}, return list of paths which are submodules."""
+ assert isinstance(tree, Tree)
+ # A simple regex to match submodule entries
+ regex = re.compile(r'160000 commit [0-9a-f]{40}\t(.*)$')
+ # First, use ls-tree to get all the trees and links
+ files = self.run(['git', 'ls-tree', '-d', '-r', '-z', tree.sha1]).output_lines('\0')
+ # Then extract the paths of any submodules
+ return set(m.group(1) for m in map(regex.match, files) if m)
def diff_tree(self, t1, t2, diff_opts, binary = True):
"""Given two L{Tree}s C{t1} and C{t2}, return the patch that takes
C{t1} to C{t2}.
diff --git a/t/t5000-refresh-submodules.sh b/t/t5000-refresh-submodules.sh
new file mode 100755
index 000000000000..9c8cbcb0b08a
--- /dev/null
+++ b/t/t5000-refresh-submodules.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+#
+# Copyright (c) 2017 Intel Corporation
+#
+
+test_description='Refresh with submodules'
+
+. ./test-lib.sh
+
+test_expect_success 'refresh with a submodule does not include by default' '
+ test_create_repo foo &&
+ git submodule add ./foo foo &&
+ git commit -m "submodule" &&
+ stg init &&
+ (
+ cd foo &&
+ touch file1 &&
+ git add file1 &&
+ git commit -m "change in submodule"
+ ) &&
+ stg new p1 -m p1 &&
+ stg refresh &&
+ [ "$(stg status)" == " M foo" ]
+'
+
+test_expect_success 'refresh includes non-submodule changes' '
+ touch file2 &&
+ git add file2 &&
+ stg refresh &&
+ [ "$(stg status)" == " M foo" ]
+'
+
+test_expect_success 'refresh with --submodules' '
+ stg refresh --submodules &&
+ [ "$(stg status)" == "" ]
+'
+
+test_expect_success 'refresh --no-submodules overrides config' '
+ stg undo && stg undo &&
+ git config stgit.refresh.submodules yes &&
+ stg refresh --no-submodules &&
+ [ "$(stg status)" == " M foo" ]
+'
+
+test_expect_success 'refresh with config' '
+ stg refresh &&
+ [ "$(stg status)" == "" ]