diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 682a03b8bf73..6f973c63e1d9 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -60,6 +60,10 @@ FILE_MAX_SIZE = 3
 ; Max number of files per upload. Defaults to 5
 MAX_FILES = 5
 
+[repository.pull-request]
+; List of prefixes used in Pull Request title to mark them as Work In Progress
+WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
+
 [ui]
 ; Number of repositories that are displayed on one explore page
 EXPLORE_PAGING_NUM = 20
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 29489d885520..18ccc87f6a2e 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -62,6 +62,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
    HTTP protocol.
 - `USE_COMPAT_SSH_URI`: **false**: Force ssh:// clone url instead of scp-style uri when
    default SSH port is used.
+   
+### Repository - Pull Request (`repository.pull-request`)
+- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
+ title to mark them as Work In Progress
 
 ## UI (`ui`)
 
diff --git a/docs/content/doc/usage/pull-request.en-us.md b/docs/content/doc/usage/pull-request.en-us.md
new file mode 100644
index 000000000000..171c944ac06b
--- /dev/null
+++ b/docs/content/doc/usage/pull-request.en-us.md
@@ -0,0 +1,31 @@
+---
+date: "2018-06-01T19:00:00+02:00"
+title: "Usage: Pull Request"
+slug: "pull-request"
+weight: 13
+toc: true
+draft: false
+menu:
+  sidebar:
+    parent: "usage"
+    name: "Pull Request"
+    weight: 13
+    identifier: "pull-request"
+---
+
+# Pull Request
+
+## "Work In Progress" pull requests
+
+Marking a pull request as being a work in progress will prevent that pull request from being accidentally merged. To mark a pull request as being a work in progress, you must prefix its title by `WIP:` or `[WIP]` (case insensitive). Those values are configurable in your `app.ini` file :
+
+```
+[repository.pull-request]
+WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
+```
+
+The first value of the list will be used in helpers.
+
+## Pull Request Templates
+
+You can find more information about pull request templates in the dedicated page : [Issue and Pull Request templates](../issue-pull-request-templates)
diff --git a/integrations/api_pull_test.go b/integrations/api_pull_test.go
index b9bab920e18f..e56b91d8b938 100644
--- a/integrations/api_pull_test.go
+++ b/integrations/api_pull_test.go
@@ -5,10 +5,13 @@
 package integrations
 
 import (
+	"fmt"
 	"net/http"
 	"testing"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth"
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/sdk/gitea"
 
 	"github.com/stretchr/testify/assert"
@@ -28,3 +31,26 @@ func TestAPIViewPulls(t *testing.T) {
 	expectedLen := models.GetCount(t, &models.Issue{RepoID: repo.ID}, models.Cond("is_pull = ?", true))
 	assert.Len(t, pulls, expectedLen)
 }
+
+// TestAPIMergePullWIP ensures that we can't merge a WIP pull request
+func TestAPIMergePullWIP(t *testing.T) {
+	prepareTestEnv(t)
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+	pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, models.Cond("has_merged = ?", false)).(*models.PullRequest)
+	pr.LoadIssue()
+	pr.Issue.ChangeTitle(owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
+
+	// force reload
+	pr.LoadAttributes()
+
+	assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
+
+	session := loginUser(t, owner.Name)
+	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, pr.Index), &auth.MergePullRequestForm{
+		MergeMessageField: pr.Issue.Title,
+		Do:                string(models.MergeStyleMerge),
+	})
+
+	session.MakeRequest(t, req, http.StatusMethodNotAllowed)
+}
diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go
index 27f3586406cc..b375a55f53b2 100644
--- a/integrations/pull_merge_test.go
+++ b/integrations/pull_merge_test.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/test"
 
+	"github.com/Unknwon/i18n"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -123,3 +124,23 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
 
 	assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
 }
+
+func TestCantMergeWorkInProgress(t *testing.T) {
+	prepareTestEnv(t)
+	session := loginUser(t, "user1")
+	testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+	testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+
+	resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
+
+	req := NewRequest(t, "GET", resp.Header().Get("Location"))
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	htmlDoc := NewHTMLParser(t, resp.Body)
+	text := strings.TrimSpace(htmlDoc.doc.Find(".merge.segment > .text.grey").Text())
+	assert.NotEmpty(t, text, "Can't find WIP text")
+
+	// remove <strong /> from lang
+	expected := i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress", "[wip]")
+	replacer := strings.NewReplacer("<strong>", "", "</strong>", "")
+	assert.Equal(t, replacer.Replace(expected), text, "Unable to find WIP text")
+}
diff --git a/models/pull.go b/models/pull.go
index 7faf1f117238..79f6d7005d56 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -214,7 +214,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
 	}
 
 	if pr.Status != PullRequestStatusChecking {
-		mergeable := pr.Status != PullRequestStatusConflict
+		mergeable := pr.Status != PullRequestStatusConflict && !pr.IsWorkInProgress()
 		apiPullRequest.Mergeable = mergeable
 	}
 	if pr.HasMerged {
@@ -1247,6 +1247,37 @@ func (pr *PullRequest) checkAndUpdateStatus() {
 	}
 }
 
+// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
+func (pr *PullRequest) IsWorkInProgress() bool {
+	if err := pr.LoadIssue(); err != nil {
+		log.Error(4, "LoadIssue: %v", err)
+		return false
+	}
+
+	for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
+		if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
+			return true
+		}
+	}
+	return false
+}
+
+// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
+// It returns an empty string when none were found
+func (pr *PullRequest) GetWorkInProgressPrefix() string {
+	if err := pr.LoadIssue(); err != nil {
+		log.Error(4, "LoadIssue: %v", err)
+		return ""
+	}
+
+	for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
+		if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
+			return pr.Issue.Title[0:len(prefix)]
+		}
+	}
+	return ""
+}
+
 // TestPullRequests checks and tests untested patches of pull requests.
 // TODO: test more pull requests at same time.
 func TestPullRequests() {
diff --git a/models/pull_test.go b/models/pull_test.go
index e725193bb176..1dad66407771 100644
--- a/models/pull_test.go
+++ b/models/pull_test.go
@@ -237,3 +237,34 @@ func TestChangeUsernameInPullRequests(t *testing.T) {
 	}
 	CheckConsistencyFor(t, &PullRequest{})
 }
+
+func TestPullRequest_IsWorkInProgress(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
+	pr.LoadIssue()
+
+	assert.False(t, pr.IsWorkInProgress())
+
+	pr.Issue.Title = "WIP: " + pr.Issue.Title
+	assert.True(t, pr.IsWorkInProgress())
+
+	pr.Issue.Title = "[wip]: " + pr.Issue.Title
+	assert.True(t, pr.IsWorkInProgress())
+}
+
+func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
+	pr.LoadIssue()
+
+	assert.Empty(t, pr.GetWorkInProgressPrefix())
+
+	original := pr.Issue.Title
+	pr.Issue.Title = "WIP: " + original
+	assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix())
+
+	pr.Issue.Title = "[wip] " + original
+	assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix())
+}
diff --git a/modules/setting/defaults.go b/modules/setting/defaults.go
index 527a6af3b9b0..7630d10090ed 100644
--- a/modules/setting/defaults.go
+++ b/modules/setting/defaults.go
@@ -5,6 +5,7 @@ import (
 )
 
 var (
-	defaultLangs     = strings.Split("en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR", ",")
-	defaultLangNames = strings.Split("English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어", ",")
+	defaultLangs                             = strings.Split("en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR", ",")
+	defaultLangNames                         = strings.Split("English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어", ",")
+	defaultPullRequestWorkInProgressPrefixes = strings.Split("WIP:,[WIP]", ",")
 )
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 4a231c1a52e7..0f57098ea61f 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -224,6 +224,11 @@ var (
 			LocalCopyPath string
 			LocalWikiPath string
 		} `ini:"-"`
+
+		// Pull request settings
+		PullRequest struct {
+			WorkInProgressPrefixes []string
+		} `ini:"repository.pull-request"`
 	}{
 		AnsiCharset:            "",
 		ForcePrivate:           false,
@@ -267,6 +272,13 @@ var (
 			LocalCopyPath: "tmp/local-repo",
 			LocalWikiPath: "tmp/local-wiki",
 		},
+
+		// Pull request settings
+		PullRequest: struct {
+			WorkInProgressPrefixes []string
+		}{
+			WorkInProgressPrefixes: defaultPullRequestWorkInProgressPrefixes,
+		},
 	}
 	RepoRootPath string
 	ScriptType   = "bash"
@@ -1031,6 +1043,8 @@ func NewContext() {
 		log.Fatal(4, "Failed to map Repository.Upload settings: %v", err)
 	} else if err = Cfg.Section("repository.local").MapTo(&Repository.Local); err != nil {
 		log.Fatal(4, "Failed to map Repository.Local settings: %v", err)
+	} else if err = Cfg.Section("repository.pull-request").MapTo(&Repository.PullRequest); err != nil {
+		log.Fatal(4, "Failed to map Repository.PullRequest settings: %v", err)
 	}
 
 	if !filepath.IsAbs(Repository.Upload.TempPath) {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d39f601a4bfb..2b04787481f7 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -842,6 +842,8 @@ pulls.tab_files = Files Changed
 pulls.reopen_to_merge = Please reopen this pull request to perform a merge.
 pulls.merged = Merged
 pulls.has_merged = The pull request has been merged.
+pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.`
+pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready
 pulls.data_broken = This pull request is broken due to missing fork information.
 pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
 pulls.can_auto_merge_desc = This pull request can be merged automatically.
@@ -849,6 +851,7 @@ pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically
 pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts.
 pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
+pulls.no_merge_wip = This pull request can not be merged because it is marked as being a work in progress.
 pulls.merge_pull_request = Merge Pull Request
 pulls.rebase_merge_pull_request = Rebase and Merge
 pulls.squash_merge_pull_request = Squash and Merge
diff --git a/public/js/index.js b/public/js/index.js
index 6c710f18f6aa..a4702a4138f7 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1655,6 +1655,23 @@ function u2fRegisterRequest() {
     });
 }
 
+function initWipTitle() {
+    $(".title_wip_desc > a").click(function (e) {
+        e.preventDefault();
+
+        var $issueTitle = $("#issue_title");
+        var value = $issueTitle.val().trim().toUpperCase();
+
+        for (var i in wipPrefixes) {
+            if (value.startsWith(wipPrefixes[i].toUpperCase())) {
+                return;
+            }
+        }
+
+        $issueTitle.val(wipPrefixes[0] + " " + $issueTitle.val());
+    });
+}
+
 $(document).ready(function () {
     csrf = $('meta[name=_csrf]').attr("content");
     suburl = $('meta[name=_suburl]').attr("content");
@@ -1869,6 +1886,7 @@ $(document).ready(function () {
     initU2FAuth();
     initU2FRegister();
     initIssueList();
+    initWipTitle();
     initPullRequestReview();
 
     // Repo clone url.
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index c346d81e33b4..1527b8e8c988 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -510,7 +510,7 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
 		return
 	}
 
-	if !pr.CanAutoMerge() || pr.HasMerged {
+	if !pr.CanAutoMerge() || pr.HasMerged || pr.IsWorkInProgress() {
 		ctx.Status(405)
 		return
 	}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 585d2f67baf8..3cce483062fd 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -356,6 +356,7 @@ func NewIssue(ctx *context.Context) {
 	ctx.Data["RequireHighlightJS"] = true
 	ctx.Data["RequireSimpleMDE"] = true
 	ctx.Data["RequireTribute"] = true
+	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
 	setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
 	renderAttachmentSettings(ctx)
 
@@ -449,6 +450,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
 	ctx.Data["RequireHighlightJS"] = true
 	ctx.Data["RequireSimpleMDE"] = true
 	ctx.Data["ReadOnly"] = false
+	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
 	renderAttachmentSettings(ctx)
 
 	var (
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
index e6592e1f5752..6a1aaf314df9 100644
--- a/routers/repo/pull.go
+++ b/routers/repo/pull.go
@@ -323,6 +323,12 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.PullReq
 		ctx.ServerError("GetPullRequestInfo", err)
 		return nil
 	}
+
+	if pull.IsWorkInProgress() {
+		ctx.Data["IsPullWorkInProgress"] = true
+		ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
+	}
+
 	ctx.Data["NumCommits"] = prInfo.Commits.Len()
 	ctx.Data["NumFiles"] = prInfo.NumFiles
 	return prInfo
@@ -516,6 +522,12 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
 		return
 	}
 
+	if pr.IsWorkInProgress() {
+		ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
+		ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
+		return
+	}
+
 	if ctx.HasError() {
 		ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
 		ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
@@ -747,6 +759,7 @@ func CompareAndPullRequest(ctx *context.Context) {
 	ctx.Data["IsDiffCompare"] = true
 	ctx.Data["RequireHighlightJS"] = true
 	ctx.Data["RequireTribute"] = true
+	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
 	setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
 	renderAttachmentSettings(ctx)
 
@@ -790,6 +803,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
 	ctx.Data["PageIsComparePull"] = true
 	ctx.Data["IsDiffCompare"] = true
 	ctx.Data["RequireHighlightJS"] = true
+	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
 	renderAttachmentSettings(ctx)
 
 	var (
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index e904ca4ba7d1..943a9b246708 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -13,7 +13,10 @@
 				</a>
 				<div class="ui segment content">
 					<div class="field">
-						<input name="title" placeholder="{{.i18n.Tr "repo.milestones.title"}}" value="{{.title}}" tabindex="3" autofocus required>
+						<input name="title" id="issue_title" placeholder="{{.i18n.Tr "repo.milestones.title"}}" value="{{.title}}" tabindex="3" autofocus required>
+						{{if .PageIsComparePull}}
+							<span class="title_wip_desc">{{.i18n.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0) | Safe}}</span>
+						{{end}}
 					</div>
 					{{template "repo/issue/comment_tab" .}}
 					<div class="text right">
@@ -150,3 +153,7 @@
 		</div>
 	</div>
 </form>
+{{if .PageIsComparePull}}
+	<script>window.wipPrefixes = {{.PullRequestWorkInProgressPrefixes}}</script>
+{{end}}
+
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 9c9dd290a393..92bfc86cb4fa 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -2,6 +2,7 @@
 	<a class="avatar text
 	{{if .Issue.PullRequest.HasMerged}}purple
 	{{else if .Issue.IsClosed}}grey
+	{{else if .IsPullWorkInProgress}}grey
 	{{else if .IsPullRequestBroken}}red
 	{{else if .Issue.PullRequest.IsChecking}}yellow
 	{{else if .Issue.PullRequest.CanAutoMerge}}green
@@ -27,6 +28,11 @@
 					<span class="octicon octicon-x"></span>
 					{{$.i18n.Tr "repo.pulls.data_broken"}}
 				</div>
+			{{else if .IsPullWorkInProgress}}
+				<div class="item text grey">
+					<span class="octicon octicon-x"></span>
+					{{$.i18n.Tr "repo.pulls.cannot_merge_work_in_progress" .WorkInProgressPrefix | Str2html}}
+				</div>
 			{{else if .Issue.PullRequest.IsChecking}}
 				<div class="item text yellow">
 					<span class="octicon octicon-sync"></span>