diff --git a/cmd/web.go b/cmd/web.go
index d3ce68d370..48622b55cd 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -206,7 +206,7 @@ func runWeb(*cli.Context) {
 		r.Post("/:org/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost)
 		r.Get("/:org/teams/:team/edit", org.EditTeam)
 
-		r.Get("/:org/team/:team",org.SingleTeam)
+		r.Get("/:org/team/:team", org.SingleTeam)
 
 		r.Get("/:org/settings", org.Settings)
 		r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost)
@@ -238,6 +238,9 @@ func runWeb(*cli.Context) {
 			r.Post("/:index/label", repo.UpdateIssueLabel)
 			r.Post("/:index/milestone", repo.UpdateIssueMilestone)
 			r.Post("/:index/assignee", repo.UpdateAssignee)
+			r.Post("/:index/attachment", repo.IssuePostAttachment)
+			r.Post("/:index/attachment/:id", repo.IssuePostAttachment)
+			r.Get("/:index/attachment/:id", repo.IssueGetAttachment)
 			r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
 			r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel)
 			r.Post("/labels/delete", repo.DeleteLabel)
diff --git a/conf/app.ini b/conf/app.ini
index 296509f721..3cea1fdd0f 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -180,6 +180,11 @@ SESSION_ID_HASHKEY =
 SERVICE = server
 DISABLE_GRAVATAR = false
 
+[attachment]
+PATH = 
+; One or more allowed types, e.g. image/jpeg|image/png
+ALLOWED_TYPES = 
+
 [log]
 ROOT_PATH =
 ; Either "console", "file", "conn", "smtp" or "database", default is "console"
diff --git a/models/issue.go b/models/issue.go
index 6253837237..90ef287cec 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -7,19 +7,24 @@ package models
 import (
 	"bytes"
 	"errors"
+	"os"
+	"strconv"
 	"strings"
 	"time"
 
 	"github.com/go-xorm/xorm"
 
 	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
 )
 
 var (
-	ErrIssueNotExist     = errors.New("Issue does not exist")
-	ErrLabelNotExist     = errors.New("Label does not exist")
-	ErrMilestoneNotExist = errors.New("Milestone does not exist")
-	ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
+	ErrIssueNotExist       = errors.New("Issue does not exist")
+	ErrLabelNotExist       = errors.New("Label does not exist")
+	ErrMilestoneNotExist   = errors.New("Milestone does not exist")
+	ErrWrongIssueCounter   = errors.New("Invalid number of issues for this milestone")
+	ErrAttachmentNotExist  = errors.New("Attachment does not exist")
+	ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
 )
 
 // Issue represents an issue or pull request of repository.
@@ -91,6 +96,14 @@ func (i *Issue) GetAssignee() (err error) {
 	return err
 }
 
+func (i *Issue) AfterDelete() {
+	_, err := DeleteAttachmentsByIssue(i.Id, true)
+
+	if err != nil {
+		log.Info("Could not delete files for issue #%d: %s", i.Id, err)
+	}
+}
+
 // CreateIssue creates new issue for repository.
 func NewIssue(issue *Issue) (err error) {
 	sess := x.NewSession()
@@ -795,17 +808,19 @@ type Comment struct {
 }
 
 // CreateComment creates comment of issue or commit.
-func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error {
+func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string, attachments []int64) (*Comment, error) {
 	sess := x.NewSession()
 	defer sess.Close()
 	if err := sess.Begin(); err != nil {
-		return err
+		return nil, err
 	}
 
-	if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
-		CommitId: commitId, Line: line, Content: content}); err != nil {
+	comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
+		CommitId: commitId, Line: line, Content: content}
+
+	if _, err := sess.Insert(comment); err != nil {
 		sess.Rollback()
-		return err
+		return nil, err
 	}
 
 	// Check comment type.
@@ -814,22 +829,38 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c
 		rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
 		if _, err := sess.Exec(rawSql, issueId); err != nil {
 			sess.Rollback()
-			return err
+			return nil, err
+		}
+
+		if len(attachments) > 0 {
+			rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)"
+
+			astrs := make([]string, 0, len(attachments))
+
+			for _, a := range attachments {
+				astrs = append(astrs, strconv.FormatInt(a, 10))
+			}
+
+			if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil {
+				sess.Rollback()
+				return nil, err
+			}
 		}
 	case IT_REOPEN:
 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
 		if _, err := sess.Exec(rawSql, repoId); err != nil {
 			sess.Rollback()
-			return err
+			return nil, err
 		}
 	case IT_CLOSE:
 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
 		if _, err := sess.Exec(rawSql, repoId); err != nil {
 			sess.Rollback()
-			return err
+			return nil, err
 		}
 	}
-	return sess.Commit()
+
+	return comment, sess.Commit()
 }
 
 // GetIssueComments returns list of comment by given issue id.
@@ -838,3 +869,138 @@ func GetIssueComments(issueId int64) ([]Comment, error) {
 	err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
 	return comments, err
 }
+
+// Attachments returns the attachments for this comment.
+func (c *Comment) Attachments() ([]*Attachment, error) {
+	return GetAttachmentsByComment(c.Id)
+}
+
+func (c *Comment) AfterDelete() {
+	_, err := DeleteAttachmentsByComment(c.Id, true)
+
+	if err != nil {
+		log.Info("Could not delete files for comment %d on issue #%d: %s", c.Id, c.IssueId, err)
+	}
+}
+
+type Attachment struct {
+	Id        int64
+	IssueId   int64
+	CommentId int64
+	Name      string
+	Path      string
+	Created   time.Time `xorm:"CREATED"`
+}
+
+// CreateAttachment creates a new attachment inside the database and
+func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	if err := sess.Begin(); err != nil {
+		return nil, err
+	}
+
+	a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path}
+
+	if _, err := sess.Insert(a); err != nil {
+		sess.Rollback()
+		return nil, err
+	}
+
+	return a, sess.Commit()
+}
+
+// Attachment returns the attachment by given ID.
+func GetAttachmentById(id int64) (*Attachment, error) {
+	m := &Attachment{Id: id}
+
+	has, err := x.Get(m)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if !has {
+		return nil, ErrAttachmentNotExist
+	}
+
+	return m, nil
+}
+
+// GetAttachmentsByIssue returns a list of attachments for the given issue
+func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) {
+	attachments := make([]*Attachment, 0, 10)
+	err := x.Where("issue_id = ?", issueId).Find(&attachments)
+	return attachments, err
+}
+
+// GetAttachmentsByComment returns a list of attachments for the given comment
+func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) {
+	attachments := make([]*Attachment, 0, 10)
+	err := x.Where("comment_id = ?", commentId).Find(&attachments)
+	return attachments, err
+}
+
+// DeleteAttachment deletes the given attachment and optionally the associated file.
+func DeleteAttachment(a *Attachment, remove bool) error {
+	_, err := DeleteAttachments([]*Attachment{a}, remove)
+	return err
+}
+
+// DeleteAttachments deletes the given attachments and optionally the associated files.
+func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
+	for i, a := range attachments {
+		if remove {
+			if err := os.Remove(a.Path); err != nil {
+				return i, err
+			}
+		}
+
+		if _, err := x.Delete(a.Id); err != nil {
+			return i, err
+		}
+	}
+
+	return len(attachments), nil
+}
+
+// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
+func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
+	attachments, err := GetAttachmentsByIssue(issueId)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return DeleteAttachments(attachments, remove)
+}
+
+// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
+func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
+	attachments, err := GetAttachmentsByComment(commentId)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return DeleteAttachments(attachments, remove)
+}
+
+// AssignAttachment assigns the given attachment to the specified comment
+func AssignAttachment(issueId, commentId, attachmentId int64) error {
+	a, err := GetAttachmentById(attachmentId)
+
+	if err != nil {
+		return err
+	}
+
+	if a.IssueId != issueId {
+		return ErrAttachmentNotLinked
+	}
+
+	a.CommentId = commentId
+
+	_, err = x.Id(a.Id).Update(a)
+	return err
+}
diff --git a/models/models.go b/models/models.go
index ded8b05984..31509ed349 100644
--- a/models/models.go
+++ b/models/models.go
@@ -36,7 +36,7 @@ func init() {
 		new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow),
 		new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser),
 		new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser),
-		new(UpdateTask))
+		new(UpdateTask), new(Attachment))
 }
 
 func LoadModelsConfig() {
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index f03aa8aeae..ba9e86dc8f 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -71,6 +71,10 @@ var (
 	LogModes    []string
 	LogConfigs  []string
 
+	// Attachment settings.
+	AttachmentPath         string
+	AttachmentAllowedTypes string
+
 	// Cache settings.
 	Cache        cache.Cache
 	CacheAdapter string
@@ -166,6 +170,13 @@ func NewConfigContext() {
 	CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME")
 	ReverseProxyAuthUser = Cfg.MustValue("security", "REVERSE_PROXY_AUTHENTICATION_USER", "X-WEBAUTH-USER")
 
+	AttachmentPath = Cfg.MustValue("attachment", "PATH", "files/attachments")
+	AttachmentAllowedTypes = Cfg.MustValue("attachment", "ALLOWED_TYPES", "*/*")
+
+	if err = os.MkdirAll(AttachmentPath, os.ModePerm); err != nil {
+		log.Fatal("Could not create directory %s: %s", AttachmentPath, err)
+	}
+
 	RunUser = Cfg.MustValue("", "RUN_USER")
 	curUser := os.Getenv("USER")
 	if len(curUser) == 0 {
diff --git a/public/js/app.js b/public/js/app.js
index 7d4e7839b4..77719efe09 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -520,6 +520,19 @@ function initIssue() {
         });
     }());
 
+    (function() {
+        var $attached = $("#attached");
+        var $attachments = $("input[name=attachments]");
+        var $addButton = $("#attachments-button");
+
+        var accepted = $addButton.attr("data-accept");
+
+        $addButton.on("click", function() {
+            // TODO: (nuss-justin): open dialog, upload file, add id to list, add file to $attached list
+            return false;
+        });
+    }());
+
     // issue edit mode
     (function () {
         $("#issue-edit-btn").on("click", function () {
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index e71c835fd8..9a265e0959 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -6,6 +6,9 @@ package repo
 
 import (
 	"fmt"
+	"io"
+	"io/ioutil"
+	"mime"
 	"net/url"
 	"strings"
 	"time"
@@ -396,6 +399,8 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) {
 		comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink))
 	}
 
+	ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes
+
 	ctx.Data["Title"] = issue.Name
 	ctx.Data["Issue"] = issue
 	ctx.Data["Comments"] = comments
@@ -670,7 +675,7 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 				cmtType = models.IT_REOPEN
 			}
 
-			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil {
+			if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil {
 				ctx.Handle(200, "issue.Comment(create status change comment)", err)
 				return
 			}
@@ -678,12 +683,14 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 		}
 	}
 
+	var comment *models.Comment
+
 	var ms []string
 	content := ctx.Query("content")
 	if len(content) > 0 {
 		switch params["action"] {
 		case "new":
-			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil {
+			if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content, nil); err != nil {
 				ctx.Handle(500, "issue.Comment(create comment)", err)
 				return
 			}
@@ -709,6 +716,24 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 		}
 	}
 
+	attachments := strings.Split(params["attachments"], ",")
+
+	for _, a := range attachments {
+		aId, err := base.StrTo(a).Int64()
+
+		if err != nil {
+			ctx.Handle(400, "issue.Comment(base.StrTo.Int64)", err)
+			return
+		}
+
+		err = models.AssignAttachment(issue.Id, comment.Id, aId)
+
+		if err != nil {
+			ctx.Handle(400, "issue.Comment(models.AssignAttachment)", err)
+			return
+		}
+	}
+
 	// Notify watchers.
 	act := &models.Action{
 		ActUserId:    ctx.User.Id,
@@ -985,3 +1010,118 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au
 
 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
 }
+
+func IssuePostAttachment(ctx *middleware.Context, params martini.Params) {
+	issueId, _ := base.StrTo(params["index"]).Int64()
+
+	if issueId == 0 {
+		ctx.Handle(400, "issue.IssuePostAttachment", nil)
+		return
+	}
+
+	commentId, err := base.StrTo(params["id"]).Int64()
+
+	if err != nil && len(params["id"]) > 0 {
+		ctx.JSON(400, map[string]interface{}{
+			"ok":    false,
+			"error": "invalid comment id",
+		})
+
+		return
+	}
+
+	file, header, err := ctx.Req.FormFile("attachment")
+
+	if err != nil {
+		ctx.JSON(400, map[string]interface{}{
+			"ok":    false,
+			"error": "upload error",
+		})
+
+		return
+	}
+
+	defer file.Close()
+
+	// check mime type, write to file, insert attachment to db
+	allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
+	allowed := false
+
+	fileType := mime.TypeByExtension(header.Filename)
+
+	for _, t := range allowedTypes {
+		t := strings.Trim(t, " ")
+
+		if t == "*/*" || t == fileType {
+			allowed = true
+			break
+		}
+	}
+
+	if !allowed {
+		ctx.JSON(400, map[string]interface{}{
+			"ok":    false,
+			"error": "mime type not allowed",
+		})
+
+		return
+	}
+
+	out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
+
+	if err != nil {
+		ctx.JSON(500, map[string]interface{}{
+			"ok":    false,
+			"error": "internal server error",
+		})
+
+		return
+	}
+
+	defer out.Close()
+
+	_, err = io.Copy(out, file)
+
+	if err != nil {
+		ctx.JSON(500, map[string]interface{}{
+			"ok":    false,
+			"error": "internal server error",
+		})
+
+		return
+	}
+
+	a, err := models.CreateAttachment(issueId, commentId, header.Filename, out.Name())
+
+	if err != nil {
+		ctx.JSON(500, map[string]interface{}{
+			"ok":    false,
+			"error": "internal server error",
+		})
+
+		return
+	}
+
+	ctx.JSON(500, map[string]interface{}{
+		"ok": true,
+		"id": a.Id,
+	})
+}
+
+func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
+	id, err := base.StrTo(params["id"]).Int64()
+
+	if err != nil {
+		ctx.Handle(400, "issue.IssueGetAttachment(base.StrTo.Int64)", err)
+		return
+	}
+
+	attachment, err := models.GetAttachmentById(id)
+
+	if err != nil {
+		ctx.Handle(404, "issue.IssueGetAttachment(models.GetAttachmentById)", err)
+		return
+	}
+
+	ctx.ServeFile(attachment.Path, attachment.Name)
+}
diff --git a/templates/repo/issue/view.tmpl b/templates/repo/issue/view.tmpl
index d95fba40d8..246055e183 100644
--- a/templates/repo/issue/view.tmpl
+++ b/templates/repo/issue/view.tmpl
@@ -62,6 +62,11 @@
                             <div class="panel-body markdown">
                                 {{str2html .Content}}
                             </div>
+                            <div class="attachments">
+                                {{range .Attachments}}
+                                <a class="attachment" href="{{.IssueId}}/attachment/{{.Id}}">{{.Name}}</a>
+                                {{end}}
+                            </div>
                         </div>
                     </div>
                     {{else if eq .Type 1}}
@@ -103,8 +108,14 @@
                                     <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div>
                                 </div>
                             </div>
+                            <div>
+                                <div id="attached"></div>
+                            </div>
                             <div class="text-right">
                                 <div class="form-group">
+                                    <input type="hidden" name="attachments" value="" />
+                                    <button data-accept="{{AllowedTypes}}" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button>
+
                                     {{if .IsIssueOwner}}{{if .Issue.IsClosed}}
                                     <input type="submit" class="btn-default btn issue-open" id="issue-open-btn" data-origin="Reopen" data-text="Reopen & Comment" name="change_status" value="Reopen"/>{{else}}
                                     <input type="submit" class="btn-default btn issue-close" id="issue-close-btn" data-origin="Close" data-text="Close & Comment" name="change_status" value="Close"/>{{end}}{{end}}&nbsp;&nbsp;